Arduino. Trucos y secretos.

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

Textos y caracteres

Es posible memorizar tanto textos como caracteres utilizando variables. Puedes incluir en una variable un único carácter o bien un texto entero o «cadena». Por carácter se entiende un único símbolo, como el símbolo utilizado para la «a» o el símbolo que representa un número (por ejemplo «1»).1 Una cadena, en cambio, es una secuencia de caracteres. En el lenguaje C, los caracteres se indican mediante comillas simples. Esta es la definición de una variable de tipo carácter:

char un_carácter = ΄a΄;

En lugar del tipo int, en esta ocasión debes utilizar el tipo char.

Una secuencia de caracteres forma un texto, que los programadores también llaman cadena. Al principio, el concepto de «cadena» no existía en el lenguaje C, que utilizaba (y todavía utiliza) secuencias de caracteres: imagina un cajón con muchos compartimentos ordenados. Una cadena se define de este modo:

char texto[] = "No tengo mucho que decir.";

Inmediatamente después del nombre de la cadena, hay que introducir un par de corchetes: el texto debe escribirse entre comillas dobles. Así, para mostrar una cadena en el Serial Monitor, puedes escribir un programa como el siguiente:

void setup() {

Serial.begin(9600);

char txt[] = "avg vgv gvg x";

Serial.println(txt);

}

void loop() {

}

Carga el programa en Arduino y, después, abre el Serial Monitor para leer el resultado.

También se puede declarar una cadena indicando previamente su longitud:

char texto[5] = "hola";

La longitud útil siempre debe contener un carácter más respecto a los necesarios. Esto se debe a que la última posición siempre debe estar ocupada por el carácter especial ΄\0΄ (denominado «carácter nulo» o «terminador»). Si no se indica de forma explícita la longitud de una cadena, será el compilador quien inserte el carácter nulo al final de la secuencia de caracteres. El carácter nulo ayuda al ordenador a saber cuándo finaliza la cadena.

Dado que una cadena es un conjunto ordenado de caracteres, puedes acceder a cada uno de los caracteres individuales indicando su posición. Puedes acceder a un único carácter escribiendo lo siguiente:

void setup() {

Serial.begin(9600);

char saludo[5] = "hola";

for (int i = 0; i < 5; i++){

Serial.println(saludo[i]);

}

}

void loop(){}

En este programa hemos creado una cadena de cinco caracteres llamada saludo. El bucle for crea un índice que se puede utilizar para leer los caracteres uno a uno y mostrarlo en el Serial Monitor con Serial.println().

El índice para acceder a los caracteres individuales de una cadena empieza de cero y llega al valor máximo, menos uno. En el ejemplo anterior, el índice va del cero al cuatro.

Arduino cuenta con un tipo String que facilita mucho las operaciones: no es necesario utilizar paréntesis o funciones raras para modificarlas. Así es como se define una cadena:

String texto = "¡Así todo es más fácil!";

Para combinar dos cadenas en Arduino, basta con sumarlas:

String txt1 = "Hello";

String txt2 = "World!";

String texto = txt1 + " " + txt2;

9. Definir una constante

Se pueden definir constantes mediante el modificador const:

const int led = 13;

o bien con la directiva #define:

#define nombre valor

Al detalle

En muchos sketch puede resultar útil definir una constante, es decir, asignar un nombre conveniente o un alias que se utilice en lugar de un número o una cadena. El uso de constantes mejora la legibilidad y el mantenimiento del código. Existen dos posibilidades para definir una constante:

•con el modificador const;

•con la directiva #define.

El modificador const se utiliza en el momento en que se declara una variable y sirve para decir a Arduino (o, mejor dicho, al compilador) que la variable es de tipo constante y que, por tanto, no sufrirá ningún cambio durante la ejecución del sketch. El compilador reservará una zona de la memoria específica para mantener este valor y, así, optimizará la escritura del código.

const int led = 13;

El clásico sketch para hacer parpadear un LED se puede reescribir utilizando una constante. Esta reescritura tiene la ventaja de simplificar el cambio de pin: si mañana quieres que parpadee un LED conectado al pin 10, no tendrás que repasar todo el sketch para ir sustituyendo el número del pin (esta ventaja también la tenemos cuando utilizamos variables); además, el uso de una constante optimiza el uso de la memoria y la velocidad del sketch:

const int led = 13;

void setup(){

pinMode(led, OUTPUT);

}

void loop() {

digitalWrite(led, HIGH);

delay(1000);

digitalWrite(led, LOW);

delay(1000);

}

El uso de la directiva #define precisa un poco más de atención, porque tiene una sintaxis diferente de la que se utiliza para las variables y es fácil cometer algún error. Con #define se pueden definir «etiquetas» que se sustituirán antes de que el sketch sea compilado, por lo que no se llega a crear una auténtica variable y el espacio de memoria consumido es solo el que ocupa el valor sustituido. Las directivas son examinadas y procesadas por el preprocesador, un programa que se ejecuta antes de la compilación real y que prepara el sketch para ser compilado.

Una #define debería permitir ahorrar un poco de memoria y agilizar la ejecución del sketch. Es recomendable insertar las constantes creadas con #define al inicio del sketch:

#define ledPin 13

Así es como sería el sketch blink utilizando una directiva en lugar de una constante:

#define LED 13

void setup(){

pinMode(LED, OUTPUT);

}

void loop() {

digitalWrite(LED, HIGH);

delay(1000);

digitalWrite(LED, LOW);

delay(1000);

}

Una #define no necesita un «tipo», sino solo una etiqueta seguida por el valor, que puede ser numérico, textual e, incluso, ¡un bloque de código! Si utilizas #define no escribas ningún signo igual ni el punto y coma final, puesto que si no estos símbolos serán sustituidos en el sketch y se producirán resultados impredecibles. La directiva #define se puede utilizar para crear macros, es decir, fragmentos de código, similares a las funciones, que implementan operaciones simples y repetidas con frecuencia dentro del sketch. Esta sería una macro que calcula la longitud de una circunferencia desde el radio:

#define cfz(R) 2*3.14*R

Una macro que seguramente encontrarás más útil es la que muestro a continuación y que sirve para simplificar la impresión sobre el Serial Monitor. En lugar de escribir cada vez Serial.println() puedes sustituir esta línea de código por un prt() más breve:

#define prt(S) Serial.println(S);

Aquí puedes ver un código completo donde se han utilizado constantes y macros:

#define PI 3.14

#define cfz(R) 2*PI*R

#define prt(S) Serial.println(S);

void setup(){

Serial.begin(9600);

int radio = 12;

String str = "circunferencia=" + String(cfz(radio));

prt(str);

}

void loop(){}

10. Manipular cadenas de caracteres

Una secuencia de caracteres forma una cadena. Arduino puede trabajar tanto con estructuras clásicas de C como las cadenas de caracteres, que no son más que matrices de char, como utilizar los objetos String que ofrecen una mayor flexibilidad. Las matrices de caracteres son muy frecuentes y, a menudo, son utilizadas por las distintas funciones y librerías. Las funciones principales para manipular este tipo de datos son:

•strcpy(dest, src): se utiliza para copiar una cadena src en una cadena dest, con las mismas dimensiones.

•strncpy(dest, src, número_de_caracteres): como strcpy copia una cadena en otra, pero pudiendo especificar el número de caracteres que hay que copiar.

•strcat(dest, src): es útil para enlazar una cadena src con otra dest.

•strcmp(str1, str2): compara una cadena con otra y, si son iguales, devuelve el valor numérico «0».

Por lo general, para desplazar las cadenas se utilizan bucles for y el acceso a los caracteres individuales se lleva a cabo mediante un índice y tratando la cadena como un simple array. Para extraer el i-ésimo carácter de la cadena str se puede utilizar:

char ch = str[i];

Al detalle

Al programar con Arduino, se puede definir una cadena, es decir, una secuencia de caracteres, de dos maneras: utilizando el método más moderno con String, un objeto que facilita muchas operaciones, o tratando las cadenas como matrices de caracteres. El segundo método es el «histórico» y estándar de C y en muchas ocasiones es preciso trabajar con estas estructuras, a veces extraídas de alguna librería.

 

Ya hemos visto que para definir una cadena de caracteres basta con declarar un array de caracteres con la longitud indicada o no:

char cadena[] = "Una simple secuencia de caracteres";

Si bien es posible trabajar con estas cadenas tratándolas como matrices y, por tanto, actuando sobre los elementos individuales, existen funciones más cómodas que agilizan algunas de las operaciones más frecuentes. Una función imprescindible es strlen(), que proporciona la longitud de la cadena, entendida como el número de caracteres contenidos hasta el carácter final de la cadena, ΄\0΄. Estas son algunas líneas de código que extraen la dimensión de la cadena y la muestran en el Serial Monitor:

char str[] = "Hello computer";

int n = strlen(str);

Serial.print("longitud de str: ");

Serial.println(n);

Para copiar una cadena se puede utilizar un bucle for que recorre la cadena de origen y la copia sobre la de destino. C ofrece un sistema más eficaz para la copia de cadenas de caracteres: la función strcpy(), que acepta como parámetros la cadena de destino y la «fuente». El comando strcpy() admite un tercer parámetro, opcional, que sirve para indicar el número de caracteres que hay que copiar. Este sería un ejemplo de uso de strcpy():

char str[] = "Hello computer";

char dest[n];

strcpy(dest, str);

Serial.print("copia de str: ");

Serial.println(dest);

La cadena de caracteres str se copia en dest con un solo comando. El uso de estas funciones es mucho más eficaz que un bucle for creado para la ocasión. Aquí tienes otro ejemplo, que utiliza strncpy() para copiar solo una parte de la cadena:

char str[] = "Hello computer";

char saludo[] = " ";

strncpy(saludo, str, 5);

Serial.print("copia de una parte de str: ");

Serial.println(saludo);

En ocasiones, puede ser necesario comparar dos cadenas. Trabajando con caracteres individuales, esta operación podría ser realmente imprescindible: deberías disponer de una cadena con la longitud adecuada, capaz de contener las dos cadenas que hay que copiar. Las dos cadenas se copian en el orden correcto dentro de la cadena final. También en esta ocasión C ofrece una solución cómoda que resuelve este problema en una única línea de código. Con strcat() se puede «pegar» la segunda cadena a la primera. Así es cómo se hace:

char a[] = "Hello ";

char b[] = "World";

strcat(a, b);

Serial.print("a + b = ");

Serial.println(a);

¿Cómo se comparan dos cadenas? C cuenta con la función strcmp() que compara carácter a carácter la primera cadena proporcionada con la segunda. Para ser idénticas, dos cadenas deben contener los mismos caracteres. La función strcmp() recorre los caracteres de las dos cadenas, los compara y devuelve «0» como resultado en el caso en que sean idénticas. Así es cómo se utiliza:

char key[] = "123";

char value[] = "123";

if (strcmp(value, key) == 0) {

Serial.print("Key y value son idénticos");

} else {

Serial.print("Key y value no son idénticos");

}

A continuación, puedes ver un sketch de resumen que contiene todas las operaciones mostradas en las líneas anteriores:

// Manipular cadenas de caracteres

void setup() {

Serial.begin(9600);

//una cadena de caracteres

char str[] = "Hello computer";

//la longitud de la cadena

int n = strlen(str);

Serial.print("longitud de str: ");

Serial.println(n);

//copia de una cadena

char dest[n];

strcpy(dest, str);

Serial.print("copia de str: ");

Serial.println(dest);

//copia de n caracteres de una cadena a otra

char saludo[] = " ";

strncpy(saludo, str, 5);

Serial.print("copia de una parte de str: ");

Serial.println(saludo);

//concatenación de dos cadenas

char a[] = "Hello ";

char b[] = "World";

strcat(a, b);

Serial.print("a + b = ");

Serial.println(a);

//comparación de dos cadenas char key[] = "123";

char value[] = "123";

if (strcmp(value, key) == 0) {

Serial.print("Key y value son idénticos");

} else {

Serial.print("Key y value no son idénticos");

}

}

void loop(){}

Habitualmente, para manipular las cadenas se utilizan bucles for o while con los cuales se escanean carácter a carácter. Para acceder a cada carácter individual, basta con tratar una cadena de caracteres como un simple array y utilizar la notación con los corchetes y un indicador str[i], como en el siguiente fragmento de código:

char str[] = "Hello!";

for (int i = 0; i < strlen(str); i++){

char ch = str[i];

Serial.println(ch);

}

11. Utilizar las cadenas

Para utilizar un objeto de tipo String, basta con definirlo mediante su constructor:

String str = String("Esto es una cadena");

O bien asignándole directamente un valor, como se hace con las cadenas de caracteres:

String str = "Una segunda cadena";

Las cadenas disponen de múltiples métodos para su manipulación que las hacen potentes y flexibles. Una de las características más apreciadas y utilizadas es la posibilidad de enlazarlas entre ellas e, incluso, con tipos de datos distintos, simplemente con un símbolo de adición, como si fuera una operación matemática común:

String a = "Hello";

String b = "World";

String c = a + " " + b;

Al detalle

En Arduino es posible definir una cadena de dos maneras: con una secuencia de caracteres o bien con el elemento String, que es un tipo de dato particular, implementado con una clase (véase 2.21), que ofrece mayores ventajas y flexibilidad respecto a la clásica secuencia de caracteres de C. Se puede definir un objeto de tipo String de varios modos. El método más natural es utilizarlo como si fuera un tipo de dato común y, después, asignarle directamente un valor:

String str = "Hello World";

Por lo general, los objetos se crean (o se implementan) llamando a su constructor, es decir, al método encargado de su creación. Se puede crear una cadena también de este modo:

String str = String("Hello Moon!");

Mediante el constructor, se pueden llevar a cabo varias posibilidades, entre las cuales la de transformar directamente varios tipos de datos en una cadena. Si el argumento del constructor es un entero, será transformado en la correspondiente cadena:

String str = String(123);

El constructor de String acepta un segundo parámetro, que según el caso puede asumir distintos significados: por ejemplo, puede servir para convertir un número de un formato a otro y mostrarlo correctamente en pantalla. Las conversiones de tipo se pueden hacer proporcionando como segundo parámetro la «base» que se debe utilizar para representar el número. Las posibilidades son: HEX, BIN y DEC. Las siguientes instrucciones transforman el número decimal «45» en una cadena con su representación hexadecimal:

String str = String(45, HEX);

Serial.println(str);

El número mostrado será «2D». De la misma forma, para obtener conversiones en binario, se utilizará BIN:

String str = String(12, BIN);

Serial.println(str);

DEC producirá un número decimal. Prueba a utilizarlo proporcionando como primer parámetro un número en notación hexadecimal:

String str = String(0xB, DEC);

Serial.println(str);

El número de caracteres incluido en una cadena se obtiene desde el método length(). Este es un ejemplo de su uso:

String str = String("Abracadabra");

int n = str.length();

Serial.print("longitud de str: ");

Serial.println(n);

Las cadenas de tipo String se pueden combinar fácilmente utilizando el símbolo de adición. Enlazar varias cadenas para componer un mensaje se convierte en una operación sencilla y rápida:

String a = "ábrete";

String b = "sésamo";

String c = a + " " + b;

Serial.println(c);

La concatenación puede incluir también variables de tipo numérico, como en el siguiente ejemplo, que permite «ahorrar» en Serial.println():

String d = "X vale ";

String e = " puntos";

int x = random(0, 10);

String f = d + x + e;

Serial.println(f);

El tipo String cuenta con muchos otros métodos que me limito a mostrar a continuación con una breve descripción de su funcionamiento. Puedes encontrar más información y detalles de su uso en la siguiente dirección: https://www.arduino.cc/reference/en/language/variables/data-types/stringobject/.

•charAt(n): devuelve el carácter a la posición indicada.

•compareTo(str): compara las dos cadenas y devuelve 0 si son iguales.

•concat(parámetro): enlaza el parámetro proporcionado, que puede ser de distintos tipos, con la cadena sobre la cual se ha llamado este método.

•endsWith(str): comprueba si la cadena termina con el carácter o la cadena proporcionada.

•equals(str): compara la cadena proporcionada y devuelve true si coinciden.

•equalsIgnoreCase(): compara dos cadenas ignorando las diferencias entre mayúsculas y minúsculas.

•getBytes(buff, n): copia n caracteres de la cadena en el búfer de byte buff.

•indexOf(str, from): busca una subcadena dentro de una cadena.

•lastIndexOf(str): devuelve la última posición del carácter o de la cadena indicados.

•length(): la longitud de la cadena.

•remove(n): elimina los caracteres de una cadena a partir de la posición n.

•replace(str1, str2): sustituye las coincidencias de str1 con str2.

•reserve(len): asigna suficiente espacio de memoria para procesar la cadena.

•setCharAt(i, ch): modifica el carácter a la posición indicada.

•startsWith(str): comprueba si una cadena empieza con la subcadena indicada.

•substring(from, to): recorta una subcadena desde la posición from hasta to.

•toCharArray(): devuelve una cadena de caracteres char[] equivalente a la actual.

•toInt(): transforma la cadena en entero.

•toFloat(): transforma la cadena en decimal.

•toLowerCase(): transforma los caracteres de la cadena de mayúsculas a minúsculas.

•toUpperCase(): transforma los caracteres de la cadena de minúsculas a mayúsculas.

•trim(): elimina los caracteres vacíos que pueden aparecer al inicio y al final de la cadena.

12. Convertir una variable de un tipo a otro

Para convertir una variable de un tipo a otro (type cast), se coloca el tipo deseado, entre paréntesis, antes de la variable a transformar:

(tipo) expresión

Al detalle

Dentro de un programa, es necesario utilizar diferentes tipos de datos. Si bien, por simplicidad, se intenta usar solo variables de tipo entero, es inevitable que se encuentren otros tipos de datos, producidos por determinadas funciones o como resultado de la realización de ciertas operaciones.

 

La conversión más simple es la explícita, en la cual se coloca, entre paréntesis, antes de la variable o número a transformar el tipo deseado. Para transformar la variable n, de tipo byte, en un entero:

byte n = 3;

int x = (int)n;

En este tipo de conversiones, denominadas en la jerga del sector type casting, se puede perder información. Si se pasa de un tipo de dato más pequeño a otro más grande no hay peligro, como en el caso que acabamos de ver: un byte ocupa solo ocho bits, mientras que un entero puede ocupar dieciséis. En una conversión de entero a byte, en cambio, podrían producirse pérdidas de datos, porque el ordenador cortaría por completo todos los bits sobrantes, manteniendo solo los primeros ocho, que se deberían copiar dentro de la nueva variable de tipo byte. Veamos un ejemplo de ello:

void setup() {

Serial.begin(9600);

int n = 12;

byte b = (byte)n;

Serial.println(b);

}

void loop(){}

La variable de tipo entero, n, ha sido convertida en bytes y, después, mostrada con Serial.println. Si cargas el sketch y abres el Serial Monitor verás que aparece el número 12. Es normal. Modifica el sketch asignando a n el valor 257.

void setup() {

Serial.begin(9600);

int n = 257;

byte b = (byte)n;

Serial.println(b);

}

void loop(){}

¡Ahora el Serial Monitor muestra «1»! Para explicar este comportamiento extraño, es aconsejable que escribas los números en bits. Así se puede escribir el número doce:

12: 0000 1100

Ocupa un único byte, porque tiene ocho bits. Si lo convertimos a tipo int, no hay ningún problema. El tipo int está formado por 2 bytes, es decir, 16 bits y los espacios vacíos se rellenarán con ceros:

12: 0000 0000 0000 1100 Ahora intentaremos hacer lo contrario, escribir el número entero 257, transformado en bits:

257: 0000 0001 0000 0001

La operación de type cast de int a byte corta por completo la variable, manteniendo solo los primeros ocho bits y dejando, por tanto, lo siguiente:

1: 0000 0001

que transformado en números decimales equivale a 1.

Podrías optar por hacer caso omiso a las conversiones de tipo, pero en ocasiones son realmente necesarias. Una conversión inevitable se da cuando se realiza una división. Al dividir dos números enteros, se puede obtener un resultado con decimales. Para no perder estos decimales, se podría guardar el resultado en una variable de tipo float:

void setup() {

Serial.begin(9600);

int n = 15;

int m = 2;

float res = n/m;

Serial.println(res);

}

void loop(){}

Ejecuta este sketch y abre el Serial Monitor. El número que aparece es 7 y no 7,5. Arduino ha realizado una división entre enteros, sin mantener los decimales. Para obtener el resultado correcto, es preciso insertar un type cast a número decimal:

void setup() {

Serial.begin(9600);

int n = 15;

int m = 2;

float res = (float)n/m;

Serial.println(res);

}

void loop(){}

Ahora la división se ejecuta correctamente y, en el Serial Monitor, se mostrará 7,5.

Otros tipos de conversiones «interesantes» son aquellas entre cadenas y números, y viceversa. A veces se reciben números en forma de texto, como cuando se lee un puerto serie o algún módulo en concreto, como un GPS que proporciona las coordenadas como cadenas de caracteres. Un número expresado en caracteres no se puede utilizar directamente para realizar cuentas, porque las cifras que vemos son solo símbolos ASCII. Para utilizarlos, se necesitan conversiones particulares. Sin duda, es más sencillo transformar un número en una cadena. Existen conversiones explícitas e implícitas. Una conversión implícita se puede obtener utilizando la concatenación de un objeto String con un número. Los objetos String son más versátiles que las cadenas de caracteres y ofrecen distintos métodos para su manipulación. Una de las propiedades de este tipo de cadenas es la de poder concatenarse utilizando el signo de adición (véase la sección dedicada a ello). Por tanto, es posible utilizar esta propiedad para transformar «implícitamente» un número en texto:

String str = String(123);

txt = String("a + " + str);

Serial.print("txt = ");

Serial.println(txt);

Sumando una cadena vacía a un número se obtendrá una cadena que contiene los caracteres que representan el número. También se puede inicializar un objeto String con un entero. El número se transformará automáticamente en su representación textual:

int n = 100;

String txt = String(n);

Convertir una cadena en un número puede ser más complicado, a menos que se utilicen los objetos String que proporcionan los prácticos métodos toInt() y toFloat() para transformar sin problemas una cadena que contiene cifras en un número. Este sería un ejemplo:

String n1 = "201";

int n = n1.toInt();

n = n + 1;

Serial.println(n);

La cadena 201 ha sido transformada en entero llamando a toInt() y después guardada en la variable, de tipo int, n. Para demostrar que la transformación ha tenido efecto, a n le sumamos «1» y después mostramos el resultado, que será 202.

String cuenta también con el método toFloat() para transformar una cadena en un número con decimales. A continuación, puedes ver un ejemplo como el anterior:

n1 = "12.3";

float x = n1.toFloat();

x = x * 2.0f;

Serial.println(x);

Con las cadenas más clásicas formadas por caracteres también es posible realizar conversiones de texto a int y float. C cuenta con las funciones atoi() y atof(), que toman como parámetro una cadena que contiene un número y la transforman en entero o decimal. Este sería un ejemplo de conversión en número entero cuyo resultado de atoi() se suma después a 100 para comprobar la efectiva conversión en número:

char n2[] = "567";

int z = atoi(n2);

z = z + 100;

Serial.println(z);

A continuación, puedes ver el correspondiente ejemplo de uso de atof():

char n3[] = "56.7";

float q = atof(n3);

q = q * 10.0f;

Serial.println(q);