featured-tuto-tiposdatos2

Introducción a los Tipos de Dato con Arduino

Los computadores, al igual que los Arduinos, tienden a ser altamente sensibles a(o dependientes de) los datos. En su núcleo, el corazón del dispositivo, existe una unidad aritmética lógica(ALU), que ejecuta operaciones (bastante) simples en la memoria: Por ejemplo R1+R2, R3*R7, R4&R5, etc. A la ALU no le importa lo que representan los tipos de datos para el usuario, sea texto, valores enteros, valores de punto flotante o incluso parte del código de programa.

Todo el contextopara estas operaciones viene desde el compilador, y es el usuario quien le da las indicaciones al compilador. Tú, el programador, le dices al compilador que tal variable es un entero y alguna otra es un número de punto flotante. Luego, el compilador se averigua a qué me refiero cuando digo “Suma este entero a ese punto flotante”. A veces es fácil, pero otras veces no. Y algunas veces pareciera que debería ser fácil, pero resulta que se terminan obteniendo resultados que quizás no esperabas.

Definiendo tipos de datos
El ambiente Arduino es realmente C++, con bibliotecas de soporte, y además asume algunos parámetros relativos al microcontrolador, para simplificar el proceso de programación. C++ define una cantidad de tipos de datos distintos; Aquí hablaremos sólo de los tipos usados en el ambiente Arduino, con énfasis en los típicos problemas para el programador principiante de Arduino.

A continuación aparece una lista de tipos de datos comúnmente utilizados en el ambienteArduino, con el tamaño de memoria de cada uno entre paréntesis.

  • boolean (8 bit)- lógico simple verdadero/falso.
  • byte (8 bit)- número sin signo entre 0 y 255.
  • char (8 bit)- número con signo, entre -128 y 127. En algunos casos el compilador intentará interpretar este tipo de dato como un caracter, lo que puede generar resultados inesperados.
  • unsignedchar (8 bit)- lo mismo que ‘byte’; si es que eso es lo que necesitas, deberías usar ‘byte’, para que el código sea más claro.
  • word (16 bit)- número sin signo entre, 0 y 65535.
  • unsignedint (16 bit)- lo mismoque ‘word’. Utiliza ‘word’ por simplicidad y brevedad.
  • int (16 bit)- número con signo, entre -32768 y 32767. Este tipo es el más usado para variables de propósito general en Arduino, en los códigos de ejemplo que vienen con el IDE.
  • unsignedlong (32 bit)- número sin signo entre 0 y 4294967295. Este tipo se usa comúnmente para almacenar el resultado de la función millis(), la cual retorna el tiempo que el código actual ha estado corriendo, en milisegundos.
  • long (32 bit)- número con signo, entre -2,147,483,648 y 2,147,483,647.
  • float (32 bit)- número con signo, entre 3.4028235E38 y 3.4028235E38. El Punto Flotante no es un tipo nativo en Arduino; el compilador debe realizar varios saltos para poder hacerlo funcionar. Evítalo siempre que te sea posible. Hablaremos de eso más tarde; En una fecha próxima se publicará un tutorial más riguroso en el uso genérico de la matemática de punto decimal en Arduino.

 

Este tutorial NO cubre arreglos, punteros o cadenas de texto. Esos tipos de dato son más específicos, e incluyen conceptos que serán vistos en un próximo tutorial.

El procesador, en el corazón de la placa Arduino (un Atmel Atmega328P) es un procesador nativo de 8 bit, sin soporte integrado para números de punto flotante. Para poder utilizar tipos de dato de más de 8 bit, el compilador debe crear una secuencia de código capaz de gestionar datos más grandes, trabajando en ellos de a poco y finalmente guardando el resultado en donde corresponda.

Esto significa que lo mejor es trabajar con valores de 8 bit, y lo peor es trabajar con punto flotante. Para demostrarlo, he escrito un sketch simple para Arduino que hace algunos cálculos matemáticos muy simples, y puede ser fácilmente modificado para usar otros tipos de dato y realizar los mismos cálculos.

Algunos conceptos básicos: Tamaño y tiempo
En primer lugar, carguemos el código tal como está en elArduino Uno, y veamos qué resultados nos entrega en la consola serial.

tut_tipos_de_dato_arduino_clip_image001

Ok, mucha información, vayamos con calma.

Primero revisa el tamaño del código compilado. Para sumas de variables de tipo byte,  el código resulta de un tamaño de 2458 bytes. No es mucho, y, francamente, la mayor parte del tamaño del código viene dada por la rutina de comunicación serial. Este punto será importante más adelante.

Ahora, veamos la salida del Puerto serial. ¿Por qué aparecen cuadrados en vez de un número correspondiente al valor de la variable? Esto ocurre porque la función Serial.print() cambia su manera de funcionar según el tipo de dato que se le entrega. Para un valor de 8 bit (sea char o un byte), la función simplemente imprime ese valor, en binario. La consola serial tratará luego de interpretar ese dato como un  carácter ASCII, y los cararacteres ASCII para 1, 2 y 3 son “INICIO DE ENCABEZADO”, “INICIO DE TEXTO”, y “FIN DE TEXTO”, respectivamente. Mmm, no muy útil o sí?, es que acaso no se puede imprimir como un solo carácter?De aquí viene el cuadrado: La consola serial levanta sus manos y dice “No sé cómo imprimir esto, así que te hice un cuadrado.” Por esto, la primera lección  para los tipos de datos en Arduino es:Para mostrar correctamente un valor decimal en la consola, se debe agregar la palabra ‘DEC’ en la llamada de la función Serial.print(), como sigue:

Serial.print(x, DEC);

Finalmente, observa la medida del tiempo transcurrido (Elapsed Time). Si es que nose consideran las imprecisiones resultantes de utilizar la función micros() para medir el tiempo transcurrido (que es lo que haremos en todos estos tests, por lo que deberíamos obtener relativamente buenas medidas del tiempo requerido por las operaciones), se puede ver que la suma de valores de 8 bit requiere aproximadamente 4 microsegundos del tiempo del procesador para computarse.

Ok, avancemos. Si es que estás siguiente el tutorial en tu casa, querrás cambiar el código, como se ve abajo:

tut_tipos_de_dato_arduino_clip_image002

tut_tipos_de_dato_arduino_clip_image003

Ahora carga el código en tu Arduino. Observa el tamaño del código compilado: 2488 bytes para int versus 2458 bytes para byte.No es mucho más grande, pero sin embargo ES más grande. Nuevamente, esto es porque usar tipos de datos que necesiten más de 8 bit de almacenamiento (como intlong o float) también requiere que el compilador genere más código de máquina para poder procesarlos. El procesador simplemente no tiene la capacidad de procesar datos grandes de forma nativa internamente. Ahora, abre la consola serial. Deberías ver algo así:

tut_tipos_de_dato_arduino_clip_image004

Siguiente observación: Esta vez los valores fueron mostrados correctamente. Esto es porque el nuevo tipo de dato que introdujimos, el int, ha sido correctamente interpretado por el compilador como un tipo de dato numérico, y la función Serial.print() formatió correctamente la información de salida para ser mostrada en la consola. Así que, la segunda lección de los tipos de datos en Arduino es: si quieres enviar el equivalente binario de un tipo de dato numérico, digamos, de manera de compartir datos con otro dispositivo, en vez de a una consola para que un usuario lo vea, usa la función Serial.write().

Luego fíjate nuevamente en el tiempo transcurrido. Esta vez el código tomó 12 microsegundos – cerca de 3 veces más tiempo! Todavía es muy poco, en todo caso. Pero esto es porque se trata de un procesador de 8 bit, como se mencionó anteriormente, por lo que se deben realizar algunos saltos para calcular matemática de 16 bit, que es lo que se necesita al sumar dos variables de tipo int.

Sigamos avanzando! Ahora veamos el tipo de dato long. Repite el último cambio del código, excepto que ahora remplaza la palabra “int” por “long”. Carga el programa y abre la consola serial para ver lo que ha ocurrido.

tut_tipos_de_dato_arduino_clip_image005

Antes de que entremos en la captura serial, revisemos el tamaño del código compilado. En mi caso esta vez obtuve un código de 2516 bytes, 28 bytes más que cuando usamos int y 58 bytes más que usando byte. Todavía es una diferencia pequeña, pero una diferencia de todas maneras, y esta diferencia puede ser mucho mayor si es que haces mucha matemática con variables de tipo long en vez de int o byte.

Ok, ahora veamos los resultados de la consola serial. Denuevo, fíjate que el tiempo transcurrido cambió. Aunque esta vez,  DISMINUYÓ de 12 microsegundos a 8! ¿Cómo pasó esto? Esta es tu tercera lección de tipos de dato en Arduino: Lo que crees que está ocurriendo puede no ser lo que en verdad sucede. No estoy seguro de por qué pasa esto – puede ser por la optimización que realiza el compilador, la cual disminuye el tiempo que toman pequeñas sumas, lo cual no ocurre con variables de tipo int.Sin embargo, decir que “long es más rápido que int”, no es una regla que siempre se cumple, como veremos cuando entremos en las multiplicaciones y divisiones.

Ok, última parada, matemática de punto flotante. Las operaciones de punto flotante en Arduino son engorrosas, porque Arduino no tiene una unidad de punto flotante, la cual es una manera elegante de referirse a una sección del procesador que se encarga de gestionar matemática con una cantidad arbitraria de dígitos decimales. La matemática de punto flotante debe usarse con precaución, porque aunque los humanos podamos tratar con un número arbitrario de ceros después de la coma, los computadores no. Este es el origen del célebre bug “1 no es 1” que algunos procesadores Pentium antiguos sufrieron.

Modifica el código nuevamente, pero reemplaza “long” por “float” en las dos secciones del código. Carga el programa y ahora fíjate que el tamaño del código compilado es 3864 bytes! Lo que pasó es que al incluir variables de tipo punto flotante, forzaste al compilador a incluir la rutina de gestión de punto flotante. Claramente esto es un gran pedazo de código – y este aumentó el tamaño notoriamente.Lección 4 de tipos de dato en Arduino: No uses matemática de punto flotante a menos que realmente la necesites. Muchas veces se requiere entregarle un feedback sobre algo al usuario, lo cual no tiene mucho sentido como un numero entero arbitrario: El ADC devolverá un valor como por ejemplo 536, el cual es críptico, pero convertido a punto flotante sería algo así como 2.62V, que claramente es mucho más útil.

tut_tipos_de_dato_arduino_clip_image006

Ahora mira la salida por consola – volvemos a 12 microsegundos. Fíjate también en que ahora el valor impreso incluye 2 ceros después de la coma. Si quieres más (o menos) dígitos después de la coma, puedes agregar un número de dígitos en la función Serial.print():

Serial.print(x, 3);   // muestra un numero de punto flotante x con 3 dígitos después de la coma.

Matemática “más difícil” – Multiplicación y División
Hasta ahora solo hemos revisado sumas. Aquí hay algunas capturas de pantalla de multiplicaciones:

tut_tipos_de_dato_arduino_clip_image007

tut_tipos_de_dato_arduino_clip_image008

tut_tipos_de_dato_arduino_clip_image009

tut_tipos_de_dato_arduino_clip_image010

Fíjate en el tiempo transcurrido: 4us para byte, 8 para int o long y 12 para float – demora más para datos más largos, y eso es también lo que esperábamos ver, la matemática “más difícil” demora más. Las multiplicaciones están soportadas por el hardware – existe una instrucción nativa en el procesador para realizar multiplicaciones con relativa facilidad. Pero ¿Qué ocurre con la división?

tut_tipos_de_dato_arduino_clip_image011

tut_tipos_de_dato_arduino_clip_image012

tut_tipos_de_dato_arduino_clip_image013

tut_tipos_de_dato_arduino_clip_image014

Oh no! Las divisiones de byte no están tan mal a 16us, pero 48 para long? Ouch. El problema aquí es que no existe una instrucción nativa en el set de instrucciones del Atmega, por lo que el compilador tiene que hacer algunos saltos para crear una. Así que la última lección es: No todas las operaciones matemáticas son creadas de la misma manera. Dividir demora más que multiplicar o sumar (o restar, pero esto es en verdad sumar con un signo negativo), y algo como calcular la raíz cuadrada o una función seno tomará incluso más tiempo. Esto toma tanto tiempo, que de hecho, resulta usualmente más fácil mantener una lista de valores para raíces cuadradas o seno/coseno/tangente y buscar el valor que quieres antes que realizar el cálculo.

Los tipos de datos son tu amigo
Lo voy a dejar hasta aquí por ahora. Espero haber demostrado con claridad los beneficios de usar el tipo de dato apropiado en tus variables. El próximo tutorial cubrirá algunas de las realmente difíciles problemáticas que aparecen al mezclar tipos de datos cuandose usan tipos inapropiados – por ejemplo, tipos de dato muy pequeños para el número más grande con que puedas encontrarte.

Todo el contexto para estas operaciones viene desde el compilador, y es el usuario quien le da las indicaciones al compilador. Tú, el programador, le dices al compilador que tal variable es un entero y alguna otra es un número de punto flotante. Luego, el compilador se averigua a qué me refiero cuando digo “Suma este entero a ese punto flotante”. A veces es fácil, pero otras veces no. Y algunas veces pareciera que debería ser fácil, pero resulta que se terminan obteniendo resultados que quizás no esperabas.