Arduino. Trucos y secretos.

Tekst
0
Recenzje
Przeczytaj fragment
Oznacz jako przeczytane
Czcionka:Mniejsze АаWiększe Aa

21. Crear clases y objetos

Para escribir programas más complejos, Arduino también permite utilizar las clases y la programación orientada a objetos (OOP). Se puede instanciar una clase con una escritura similar a la que se utiliza para crear una variable:

Led led1 = Led();

La escritura de una clase utiliza una declaración en la cual se insertan los métodos y las variables que pueden ser de tipo privado, público o protegido:

class Led {

private:

int status; //propiedad

public:

Led(); //constructor

void turnOn(); //método

};

La implementación de los métodos es similar a la definición de una función. El nombre del método siempre va precedido del nombre de la clase:

void Led::setState (int s) {

status = s;

}

Al detalle

Hace unas décadas se descubrió que, para escribir programas de un modo más fácil, se podía utilizar un nuevo método: la programación orientada a objetos (OOP – Object Oriented Programming). En nuestro mundo interactuamos continuamente con objetos. Los objetos tienen propiedades, como color, forma, peso, precio, etc. Interactuamos con los objetos realizando acciones: los movemos, nos los comemos, los creamos, los destruimos, los cambiamos de color...

Alguien pensó que todo esto se podía reproducir también en la programación ofreciendo a los programadores la posibilidad de definir objetos de tipo software dotados de métodos y propiedades.

Las propiedades son simples variables, mientras que los métodos son funciones. Los objetos esconden en su interior los detalles del funcionamiento y exponen «métodos» para poder utilizarlos e interactuar con ellos.

Cada objeto debe, antes que nada, estar definido en su «forma»: es necesario establecer qué métodos y propiedades posee. Esta definición se lleva a cabo escribiendo una clase. La clase es como si fuera el proyecto por seguir para construir el objeto. Para poder utilizar el objeto, es preciso crear una instancia de la clase. Cada instancia tiene una existencia propia que desarrolla independientemente de las de las otras instancias creadas a partir de la misma clase.

Podemos definir un objeto que duplica un automóvil. Para ello, voy a escribir una serie de instrucciones que definen la clase. En la Fiat de Turín tienen un cajón con los proyectos del «Fiat 500». Cuando alguien pide un 500 rojo, toman los proyectos y construyen un coche pintándolo de color rojo. Así es como, en el cajón, tenemos la definición de la clase «auto Fiat 500» y, por la calle, en cambio, encontramos distintas instancias del Fiat 500: rojos, azules, a topos o a rallas. La programación orientada a objetos funciona del mismo modo. El programador ha escrito la definición de la clase de un objeto y, cada vez que lo necesita, crea una instancia de ella que es totalmente distinta a las otras y que tiene una vida propia.

Para crear un ejemplo con Arduino podríamos pensar que tenemos una clase «Led» que contiene variables típicas para un componente electrónico: modelo, color, estado (encendido o apagado) e, incluso, corriente y tensión aplicada. Las variables normalmente no se modifican directamente, sino mediante «métodos». Antes que nada, es necesario crear una nueva instancia de «Led» a partir de su clase. La escritura que se utiliza para instanciar un objeto a partir de una clase es similar a la que se utiliza para crear una nueva variable:

Led led1 = Led();

En primer lugar, encontramos el nombre de la clase «Led», seguido del nombre que queremos dar a esta instancia (led1). La operación es similar a la creación de una variable y el nombre del objeto utiliza las mismas reglas para la nomenclatura. Tras el signo igual, vemos, de nuevo, el nombre de la clase seguido de dos paréntesis: es el constructor, es decir, el método principal de la clase, que permite construir la instancia inicial del objeto. En este caso, el constructor no tiene parámetros, aunque a veces es posible añadirlos (depende de cómo se ha escrito la clase y si esta los admite). Para la clase «Led» podríamos tener un parámetro que defina el color del componente:

Led led1 = Led("ROJO");

O bien el pin al cual está conectado:

Led led1 = Led(13);

Las informaciones pasadas al constructor se mantienen dentro de la estructura del objeto utilizando simples variables. Para modificar las variables se pueden utilizar los métodos, que son funciones especiales definidas dentro de la clase a la que pertenece el objeto. Esta particularidad, la de mantener «en privado» algunas informaciones respecto a los usuarios, se conoce como encapsulamiento. Para encender y apagar el LED hago lo siguiente:

led1.turnOn();

led1.turnOff();

Los métodos han sido definidos por el programador cuando ha escrito la definición de clase. Quien utiliza la clase no tiene ni idea (ni le interesa saberlo) de cómo se han realizado estos métodos. Por tanto, los objetos son como «cajas negras» de las cuales ignoramos el funcionamiento y la organización interna. El único modo para interactuar lo proporciona un conjunto de métodos que definen la interfaz del objeto.

La definición de una clase en C++ empieza con la palabra clave class, seguida del nombre de la clase y de un par de llaves. Dentro del bloque delimitado por las llaves insertaremos las variables y la declaración de los métodos presentes.

class Led {

private:

int status;

int led;

public:

Led();

Led(int);

void turnOn();

void turnOff();

void setState(int);

int getState();

String toString();

};

En el cuerpo de la definición puedes ver unas palabras especiales: public y private. Estas palabras clave sirven para indicar qué partes serán visibles externamente y cuáles no. Las propiedades del objeto Led son status y led (el pin al cual estará conectado) y no nos interesa que los usuarios puedan acceder directamente a ellos, por lo que las protegemos con la palabra clave private, la cual oculta su visibilidad. Inmediatamente después de la palabra clave public encontramos el constructor Led() y los métodos que queremos que sean públicos o que estén expuestos a los usuarios. El constructor tiene una sintaxis particular respecto a las funciones, como el hecho de que no utiliza ningún tipo de retorno ni void. En la clase Led encontramos dos constructores. El segundo utiliza un parámetro de tipo int que servirá para especificar el pin al cual se encuentra conectado el LED.

Después de haber declarado los métodos que están presentes en la clase, pasamos a su implementación. Para que el compilador reconozca los métodos que pertenecen a la clase, la definición siempre va precedida del nombre de la clase seguido de un par de «dos puntos». Esta es la definición del constructor simple:

Led::Led(){

status = LOW;

pin = 13;

}

La escritura es similar a la que se utiliza para definir una función, solo que el nombre del método o del constructor siempre va precedido del nombre de la clase. Este constructor utiliza las variables internas status y pin, asignándoles valores iniciales. El estado status se define en LOW y el pin predefinido es el número 13. El segundo constructor espera que se indique el pin al que se debe conectar el LED; su implementación será:

Led::Led(int n){

status = LOW;

pin = n;

}

El pin, pasado como parámetro n, se copiará en la variable interna y privada pin. Las variables internas siempre se deberían modificar solo utilizando los métodos previstos por la clase. Así, para modificar el estado del LED utilizaremos los siguientes métodos:

void Led::turnOn() {

status = HIGH;

}

void Led::turnOff() {

status = LOW;

}

También he utilizado un método para definir el estado según convenga:

void Led::setState (int s) {

status = s;

}

Y otro para comprobar en qué estado se encuentra el LED y que devuelve el valor de status (HIGH o LOW):

int Led::getState () {

return status;

}

Por último, resulta conveniente tener un método que ayude a mostrar en pantalla, por ejemplo, en el Serial Monitor, las informaciones del LED:

String Led::toString(){

String s = "led is ";

if (status == LOW) s += "LOW";

else s += "HIGH";

s += " connected on pin ";

s += String(pin);

return s;

}

Para utilizar la clase, instanciar un objeto y encender el LED, haremos lo siguiente:

Led led = Led(13);

led.turnOn();

Serial.println(led.toString());

A continuación, muestro el sketch completo:

class Led {

private:

int status;

int pin;

public:

Led();

Led(int);

void turnOn();

void turnOff();

void setState(int);

 

int getState();

String toString();

};

void setup(){

Serial.begin(9600);

Led led = Led(13);

Serial.println(led.toString());

led.turnOn();

Serial.println(led.toString());

}

void loop(){}

Led::Led(){

status = LOW;

}

Led::Led(int n){

pin = n;

status = LOW;

}

void Led::turnOn() {

status = HIGH;

}

void Led::turnOff() {

status = LOW;

}

void Led::setState (int s) {

status = s;

}

int Led::getState () {

return status;

}

String Led::toString(){

String s = "led is ";

if (status == LOW) s += "LOW";

else s += "HIGH";

s += " connected on pin ";

s += String(pin);

return s;

}

Los objetos también pueden crear jerarquías. Podríamos tener un primer objeto más sencillo que podríamos utilizar como «apoyo» para crear algo más complejo. Por ejemplo, podríamos tener un objeto de tipo ComponenteElectrónico que utiliza propiedades como corriente, tensión y potencia. Este objeto podría estar «especializado» por otros objetos «derivados», como los objetos «Led» o «Resistor». Cada uno de estos objetos especializados contará con las propiedades heredadas como tensión y corriente y, sin embargo, podrá añadir otras más específicas. El objeto «Led» podría incluir la propiedad color y el objeto «Resistor» podría tener una propiedad resistencia. En la definición de una clase, se puede utilizar la palabra clave protected para que algunas propiedades y métodos sean privados para aquel que utiliza directamente la clase, pero siguen siendo accesibles, en cambio, para quien la desea ampliar.

La información incluida en esta sección es muy limitada. La programación orientada a objetos es un método complejo y articulado. Si deseas profundizar en este tema, deberás consultar un libro que trate explícitamente sobre ello.

22. Generar números aleatorios

Para generar números aleatorios, podemos utilizar la función random():

long n = random(max);

Esta función genera números comprendidos entre 0 y, como máximo, –1. Es posible generar números comprendidos en un intervalo que va desde un mínimo hasta un máximo -1:

long n = random(min, max);

Además, random() genera números pseudoaleatorios y, si deseamos tener números aleatorios de verdad, es recomendable inicializar el algoritmo con:

randomSeed(analogRead(0));

Al detalle

Para generar un número aleatorio, existe una función random(), incluida en las librerías de Arduino, que proporciona un número aleatorio extraído de una secuencia de números generados por un algoritmo. Para generar un número entero de forma aleatoria entre 0 y 99 se utiliza el siguiente texto:

int n = random(100);

También se puede indicar un rango de valores a partir del cual se deben extraer los números. Para obtener números entre 1 y 4, deberás escribir lo siguiente:

int n = random(1,5);

El límite superior siempre se excluye del campo de los valores generados. Los números objetivos son números pseudoaleatorios, porque los genera un algoritmo. ¡Si no tomas las precauciones adecuadas obtendrás siempre las mismas secuencias! Antes de llamar a random() siempre es recomendable inicializar la secuencia con randomSeed(), que también requiere un número aleatorio para ser inicializada. El mejor método para obtener un parámetro aleatorio para inicializar randomSeed() es realizar una lectura analógica. Si las entradas analógicas no están conectadas, solo registran ruido, óptimo para obtener un número aleatorio real. Por tanto, la inicialización de random() se puede realizar así:

randomSeed(analogRead(0));

Aquí puedes ver un sketch completo para mostrar en el Serial Monitor una secuencia de números aleatorios:

long numero;

void setup(){

Serial.begin(9600);

randomSeed(analogRead(0));

}

void loop() {

numero = random(100);

Serial.println(número);

delay(100);

}

Al inicio del sketch preparamos una variable long para guardar los números generados. En el setup() encontramos la inicialización del puerto serie y de la serie de números aleatorios, mediante randomSeed() a la cual hemos proporcionado una lectura analógica tomada de A0. Esta operación «mezcla» los números generados por el algoritmo. En la sección loop(), random(100) genera números aleatorios entre 0 y 99. Los números se muestran en el Serial Monitor.

23. Encontrar el número más alto en un array

Para encontrar el número más alto contenido en un array hay que crear una variable que guarde el resultado y, después, recorrer todos los elementos del array. Cada vez que el elemento actual es mayor que el valor guardado, se sobreescribe.

Al detalle

Los algoritmos de búsqueda siempre son críticos debido al tiempo necesario para recorrer las estructuras de datos. Un caso muy sencillo que ocurre a menudo en programación es uno donde se debe buscar el valor máximo dentro de un array. El método más intuitivo consiste en recorrer la secuencia de valores en busca de aquel que debería ser el número más alto. Para los seres humanos, es sencillo localizar el número más alto dentro de una lista. Si la lista no es muy extensa, basta con un vistazo; si no, nos toca recorrer todos los elementos, uno a uno, hasta que encontramos el que tiene el valor más alto. Un ordenador no puede echar un vistazo a toda una lista y, por tanto, el algoritmo de búsqueda debe «razonar» como si el array fuera parcialmente visible. Imagina que solo puedes leer una casilla del array y que, por tanto, el único elemento visible es el actual. La mejor estrategia para afrontar esta situación de «visibilidad limitada» es disponer de una hoja de papel donde anotar el que, para nosotros, hasta el momento actual, podría ser el número más alto de la lista. Así, imagina que estás sentado frente a una ventana, con tu hoja de papel y un lápiz. Al otro lado de la ventana, un amigo te va mostrando números, uno a uno. Cuando el juego termina, debes indicar cuál es el número más alto entre todos los presentados. No sabes cuántos números te enseñará, pero tu amigo te avisará cuando se terminen. ¿Preparado? ¡Empezamos! El primer número que aparece es un cero, por lo que escribes «0» en tu hoja de papel. Llega el segundo número: «10», que es sin ninguna duda mayor que 0, por lo que este podría ser el número más alto. Borras el 0 y escribes un gran 10. Después aparece un 5, que es inferior a 10 y, por tanto, no puede ser el número más alto. Dejas escrito el 10. Ahora aparece el 56... y lo apuntas de inmediato en el papel. El siguiente número es un 20, por lo que no haces nada. Tu amigo te avisa de que los números han terminado y tú ya puedes afirmar que el número más alto de la serie es el 56.

Veamos cómo traducir en código este algoritmo. Preparamos un array donde insertar la secuencia de números enteros:

int sec[] = {0, 10, 5, 56, 20};

Ahora necesitamos una variable en la cual se memorice el resultado, es decir, el número con el valor más alto de todo el array. Definimos una variable max de tipo entero:

int max = 0;

Como no disponemos de la versión «arduinizada» de un amigo que pasa los números desde el otro lado de la ventana, nos contentaremos con utilizar un bucle for para recorrer los números de la lista y leerlos uno a uno. El bucle parte de 0 y llega al final del array. Podemos hacer que el programa calcule automáticamente el tamaño del array así:

sizeof(seq)/sizeof(int)

Un array se define especificando el número de posiciones, mientras sizeof(), una función predefinida de C, mide las dimensiones de una variable o de un array en bytes. ¡El operador sizeof(seq) no devolverá el número de celdas del array, sino su tamaño en bytes! Si tenemos en cuenta que un int, en Arduino, ocupa dos bytes, podemos utilizar sizeof() para calcular el número de celdas reales del array.

for (int i = 0; i < (sizeof(seq)/sizeof(int)); i++){

//código…

}

En cada iteración, tendremos que comprobar si el valor actual (seq[i]) es mayor que el valor memorizado dentro de la variable max. Para ello, utilizamos un simple if.

if (seq[i] > max) {

//guardo el valor actual…

}

Si la condición se verifica, sobreescribimos el valor de max con el actual de la secuencia.

max = seq[i];

Este es el código completo:

int seq[] = {0, 10, 5, 56, 20};

void setup() {

Serial.begin(9600);

//la variable con el número máximo:

int max = 0;

for (int i = 0; i < (sizeof(seq)/sizeof(int)); i++){

if (seq[i] > max) {

//si el número actual es mayor que el

//guardado dentro de max, lo copio

max = seq[i];

}

}

Serial.println("El max: ");

Serial.println(max);

}

void loop(){}

El valor encontrado aparece en el Serial Monitor. Con pequeñas modificaciones, el mismo algoritmo se puede utilizar para buscar un valor determinado, así como caracteres dentro de una cadena.

24. Ordenar una matriz de números

BubbleSort u ordenamiento de burbuja es un algoritmo de ordenación muy conocido. Una de sus implementaciones para Arduino para ordenar una lista de números enteros es esta:

int a[] = {1,20,15,2,7};

int c = 0;

while (c < sizeof(a)/sizeof(int) ){

//muestra matriz

muestraMatriz();

int i = 0;

while (i < (sizeof(a)/sizeof(int) - 1) ){

if (a[i] > a[i+1]){

//realizo el intercambio

int n = a[i];

a[i] = a[i+1];

a[i+1] = n;

}

i++;

}

c++;

}

Al detalle

Ordenar datos es una de las especialidades de ordenadores y microcontroladores. Existen muchos algoritmos para ordenar listas de números que se diferencian por su rapidez y efectividad. Un buen algoritmo realiza su tarea con pocas iteraciones y, sobre todo, con un número de pasos predecible con anterioridad. Algunos lenguajes de programación incluyen en sus librerías algoritmos para ordenar directamente los tipos de datos tratados, para que no sea necesario escribir el código desde cero cada vez que se necesite. Un algoritmo de ordenación muy conocido es BubbleSort, cuyo nombre evoca las burbujas que se elevan hacia arriba mientras se lleva a cabo la ordenación. Para comprender su funcionamiento, nos basamos en una lista de números:

20,15,1,7

El BubbleSort recorre el array varias veces y cada vez que encuentra una situación anómala intenta solucionarla «localmente». Es decir, analiza dos números adyacentes y, si no están en orden, los cambia de posición. Si se repite esta operación durante un determinado número de pasos, se consigue ordenar todo el array porque los números «se desplazan» en un sentido o en el opuesto, ordenándose.

Empecemos a analizar la lista de números. Vamos a imaginar que cogemos el primer número de la lista y lo comparamos con el siguiente. Si el siguiente es menor, los cambiamos de posición.

Nuestro array inicial:

20,15,1,7

pasará a ser:

15,20,1,7

El 15 y el 20 han intercambiado sus posiciones y parece que el 20 «se desplace» hacia la derecha. Ahora, tomamos el número 20, que está en segunda posición, y lo comparamos con el número de su derecha: 1. Si 1 es menor que 20, los cambiamos de posición.

 

15,1,20,7

Repetimos la comparación una última vez: 20 > 7 y, por tanto, de nuevo los cambiamos de posición. El número 20 ha acabado en último lugar.

15,1,7,20

Ahora volvamos a empezar. El primer número es 15 , que es mayor que 1. Los cambiamos de posición y seguimos adelante.

1,15,7,20

En el siguiente paso encontramos 15 y 7, y los cambiamos de posición.

1,7,15,20

Ahora tenemos 15 y 20, pero 20 es mayor que 15, por lo que nos detenemos y empezamos de nuevo. Esta vez realizaremos una vuelta inútil porque 1 < 7, 7 < 15 y 15 < 20 y, por tanto, no haremos ningún cambio. La lista está ordenada.

Para estar seguros de que la hemos ordenado, en el peor de los casos, con una lista de N elementos se necesitan N-1 pasos. Con nuestra lista necesitamos solo cuatro pasos. A continuación, puedes ver el sketch completo:

int a[] = {1,20,15,2,7};

void setup(){

Serial.begin(9600);

int c = 0;

while (c < sizeof(a)/sizeof(int) ){

//muestra matriz

muestraMatriz();

int i = 0;

while (i < (sizeof(a)/sizeof(int) - 1) ){

if (a[i] > a[i+1]){

//realizo el intercambio

int n = a[i];

a[i] = a[i+1];

a[i+1] = n;

}

i++;

}

c++;

}

Serial.println("Resultado final:");

muestraMatriz();

}

void loop(){}

void muestraMatriz(){

Serial.print("[");

for (int j = 0; j < sizeof(a)/sizeof(int); j++) {

Serial.print(a[j]);

if (j < sizeof(a)/sizeof(int) - 1) Serial.print(",");

}

Serial.println("]");

}

El array está situado al inicio del sketch. He añadido una función de conveniencia, para simplificar la escritura del código, que muestra en el Serial Monitor los elementos del array. La función se denomina muestraMatriz() y se encuentra al final del sketch. Todo el código forma parte del setup(). El recorrido del array se lleva a cabo con dos bucles while. El bucle más interno utiliza la variable-índice i y, en cada paso, compara el elemento a[i] con el siguiente a[i+1]. Por este motivo, el bucle while no llega hasta el final de la lista, sino que se detiene un paso antes. En el cuerpo del while, si el valor que contiene a[i+1] es menor que el que contiene a[i], los dos números intercambian sus posiciones. Para realizar el intercambio se necesita una variable de apoyo, n, en la cual se copia el valor de a[i] que, después, será sobreescrito por el de a[i+1]. El bucle while externo sirve para recorrer el array el número suficiente de veces para garantizar la ordenación.

Si ejecutas el algoritmo verás que el array quedará ordenado tras unos pocos pasos y que, sin embargo, el algoritmo lo recorrerá otras veces sin hacer nada. Podríamos mejorar el sketch insertando una prueba que detecte el momento en que la lista de números ya está ordenada y, por tanto, interrumpa la ejecución.

Aquí tienes el código final al cual he añadido una variable m que aumenta cada vez que se produce un intercambio de números:

int a[] = {1,20,15,2,7};

int c = 0;

void setup(){

Serial.begin(9600);

while (c < sizeof(a)/sizeof(int) ){

//muestra matriz

muestraMatriz();

int m = 0; //sirve para interrumpir la ejecución

//cuando la lista ya está ordenada

int i = 0;

while (i < (sizeof(a)/sizeof(int) - 1) ){

if (a[i] > a[i+1]){

//realizo el intercambio

int n = a[i];

a[i] = a[i+1];

a[i+1] = n;

//si he intercambiado números, incremento m

m++;

}

i++;

}

//si m = 0 el array está ordenado y, por tanto, me detengo

if (m == 0) break;

c++;

}

Serial.println("Resultado final:");

muestraMatriz();

}

void loop(){}

void muestraMatriz(){

Serial.print("[");

for (int j = 0; j < sizeof(a)/sizeof(int); j++) {

Serial.print(a[j]);

if (j < sizeof(a)/sizeof(int) - 1) Serial.print(",");

}

Serial.println("]");

}

Si m no recibe ningún incremento y, por tanto, vale «0», significa que el array ya está ordenado y se puede interrumpir el proceso con un break.