Serial Arduino parte II, tratará las diversas funciones para el tratamiento de Cadenas, y que ya hemos introducido el la primera parte Arduino Serial Parte I Veremos la manipulación de cadenas , y arreglos o arrays[], y si vamos un poco mas profundo, el uso de punteros. En esta entrada vamos a cubrir la función Serial.read() de lectura de datos y su relación con las cadenas.
Lo primero que vamos a ver es la función Serial.read() , luego lo que son los arrays[], como se definen e inicializan. Teniendo un conocimiento de los arrays[], después podemos introducirnos en el mundo de las Cadenas que son un caso especial de los arrays[], y mas tarde con los punteros dada su similitud con las cadenas que conceptualmente están muy ligados dado que el elemento de la cadena es un puntero indexado.
Serial.read()
Esta función lee de a un Byte únicamente, y al hacerlo lo extrae del buffer. Recordemos que un dato que enviado desde el Monitor Serial es primero interpretado como ASCII y el monitor lo que hace es enviar los Bytes convertidos. Por ejemplo si enviamos un 1, el monitor interpretará que es un ASCII, así, enviará el entero 49 que es el ASCII (1). Después resta, como nosotros desde Arduino, lo interpretemos o que tipo de variables utilicemos para recuperarlo. El monito de Arduino cuando envía datos, lo hace de acuerdo a la configuración que tengamos seteada en dicho monitor. Si observamos tenemos varias opciones de configuración:
Para nuestro análisis carguemos el siguiente código donde Arduino lo que hace es renviar como loop todo lo mismo que recibe
- Ambos NL & CR : Esta opción agrega un carácter de CR o carriage Return (13)( 0D)(‘\r’) y un carácter LF o LineFeed (10)(0A)(‘\n’) al final de todo lo que enviemos.
Al enviar solo un “1” en la caja de Enviar, recibimos
49 ASCII de 1
13 CR
10 LF
- Sin Ajuste de Línea: Esta opción no envía ni CR ni LF al final de lo que enviemos.
Enviamos un “1” Recibimos simplemente 49
- Nueva Línea: Envía solo LF al final de lo que enviemos.
Enviamos un «1» y Recibimos
49
10 LF/NL
- Retorno de Carro: Esta opción envía CR al final de lo que enviemos
Recibimos
49
13 CR
Por Default viene Ambos NL y CR.
Enviemos la cadena “HOLA” por default y coloquemos print , no println:
Recibimos: 72 79 76 65 13 10
La pregunta del millon: ¿Porque si enviamos el «1» no volvemos a ver el «1»?. La respuesta está en Arduino Serial Parte I. Recordar que al recibir el 49 , lo recibe como un entero y luego cuando lo reenvía lo hace con ASCII(4)-ASCII(9).
Serial.available() Esta función retorna 1 si existen bytes disponibles para ser leídos , caso contrario devuelve -1 y hay que tener mucho cuidado cuando se la usa con While() debido a que la velocidad de ejecución del micro es muy superior a la velocidad con que se reciben o transmiten los datos por el Serial Port. Supongamos el siguiente programa:
En primera instancia dejemos delay(100) comentada, para que no se ejecute.
Al ejecutar , abrimos el monitor y enviemos nuevamente el “1”.
49
Ciclo:0
13
Ciclo:1
10
Ciclo:2
No estaría realizando lo que pedíamos, ya que saldríamos de While si no hay más datos, como vienen 49-13-10, esperaríamos que el ciclo 1 se ejecute después de recibir estos caracteres. Esto se produce debido a las diferencias de velocidad con que ejecuta Arduino y la velocidad con que se reciben los datos. El clock del Arduino=16Mhz, supongamos que 1 ciclo de instrucción como en la mayoría de los micros sea de ¼ Fclok = 4Mhz, esto implica que un ciclo de instrucción dura 0.25 microsegundos. De acuerdo al compilador una instrucción puede demorar varios ciclos de instrucción. Supongamos que el While() demora 10 ciclos de instrucción = 2.5 us. Un dato Byte 8N1 a 9600 demora en llegar (8+1)*1/9600=937 us. Ni bien se reciba el 49 , se imprime y el while() vuelve a ejecutarse ,pero no habrá datos disponibles hasta 937us , en este caso el CR y, por lo tanto el flujo del programa sale del While dado que el available=-1, y así ejecuta el ciclo 1 del for, pero como los datos todavía no están disponibles quedan en el primer While esperando. Si sumamos un pequeño retardo dentro del while , descomentando la línea delay(100) ( con un par de milisegundos bastaría); obligamos a no seguir la ejecución para dar tiempo a que llegue el segundo carácter .Ejecutamos ahora veremos lo que queremos ver
49
13
10
Ciclo:0
Así que ha tener cuidado con estas pequeñas cosas que son grandes dolores de cabeza.
Arrays[]
Los arrays[] son arreglos de datos , también llamados vectores, en este caso de una dimensión. El compilador reserva una cierta cantidad de bytes consecutivos en la memoria para este tipo de datos, por lo tanto hay que especificarle una dimensión, es decir la longitud máxima del mismo. Los arrays pueden contener cualquier tipo de dato, inclusive Objetos. Los datos que contiene el array[] se llaman elementos del arreglo y se numeran como 0,1,2,3,4,…….(N-1), donde N es la dimensión. Puede verse al array[] como un edificio de N pisos, cada piso tiene un departamento , 1 elemento donde vive una persona, entonces podemos hacer la analogía de la siguiente manera para un edificio de 10 pisos:
Edificio[10], quiere decir que N=10, que es su longitud, y sus elementos será 10 obviamente, uno en cada piso numerados del 0 al 9 (N-1)
Edificio[0] vive Juan
Edificio[1] vive Ana
Edificio[2] vive Antonio
. . . .
Edificio[9] vive Mirta
Declaración e inicialización de Arreglos
Existen varias maneras de declarare inicializar datos a los arreglos, lo primero que debemos especificar es el Tipo de dato que va a contener el array[], es decir, siguiendo con la analogía del edificio, que es lo que “vive” o se aloja en cada departamento. Lo segundo es la dimensión.
<Tipo de dato> nombreArray [Dimensión];
int arreglo[10]; // De esta manera especificamos que cada elemento será un entero.
float arreglo[20]; // Cada elemento será un float. De igual manera podemos especificar tipos como String (Cadenas), char, punteros, etc, etc.
Inicialización de Arrays
Se pueden declarar arrays[] y asignar más tarde o directamente declarar y asignarle los elementos, vemos los casos posibles:
Caso 1: Declaración solamente y posterior inicialización
Int precios [4];
precios[0]=10; precios[1]=24; precios[2]=10045; precios[3]=0;
Caso 2: Declaración e inicialización
Int percios[4]={10,24,10045,0};
Caso 3: Declaración e inicialización ( sin dimensión)
Int precios[]={10,24,10045,0};
Recorriendo Arrays[]
El array[] se accede mediante el índice de los elementos, dicho índice x es un puntero dentro del array[x] que apunta al contenido en la posición x. Si int precios [4]={10,24,10045,0}; precios[1] será 24, el índice x=1. Vemos como x es un puntero índice dentro del array. Entonces podemos recorrer un array[] desde x=0 hasta x=Dimensión-1.
Cada 500 ms se imprimirá el vector completo. También podemos armar arrays[] de texto o cadenas de caracteres o simplemente cadenas. Si bien no hablamos del objeto String, es fácilmente entendible.
Cadenas
Las cadenas, como fue introducido, son conjunto de caracteres con la característica que poseen un carácter especial de terminación de cadena ‘\0’ o llamado carácter Null de fin de cadena o ASCII(0). Existen varias maneras de definir cadenas.
Lo que distingue simplemente un array de caracteres y una cadena , es que la cadena es un array de caracteres + el último elemento Null o ‘\0’.
String datosCadena=»soy cadena también”;
char arreglo[]={‘n’,’o’,’-‘, ‘s’,’o’,’y’,’-‘, ‘c’,’a’,’d’,’e’,’n’,’a’};
char cadena[]={‘s’,’o’,’y’,’-‘, ‘c’,’a’,’d’,’e’,’n’,’a’,’\0′};
char otracadena[]=»Soy otra Cadena»;
arreglo es simplemente un vector de caracteres NO una cadena. Cadena si es una cadena. datosCadena y otracadena también son cadenas.
La diferencia es que en caso de definirlas con arrays[] hay que incluír el ‘\0’ final de cadena.
Esto es hablando extrictamente del Ansi “C” o standard “C”. Este carácter no se visible y tampoco se puede imprimir ya que se usa como fin de cadena. Las funciones print y println están compliladas en sus librerías de manera buscan ese carácter para saber cuando finaliza la impresión, o la unión de cadenas, las funciones que manipulan cadenas en general. Cuando deben mandar algo al puerto Serie , previamente le agregan en Null antes de llamar a la librería, en el sistema Arduino ( Ojo esto es solo en las funciones print/println) ya que tratan al argumento como cadenas, no ocurre así con otras funciones que manipulan cadenas pues suponen que lo son.
Veamos este ejemplo y nos queda mas claro:
Tenemos un arreglo de caracters sin fin de cadena y datos que es una cadena
char arreglo[]={‘n’,’o’,’-‘, ‘s’,’o’,’y’,’-‘, ‘c’,’a’,’d’,’e’,’n’,’a’};
char otroarreglo[]={‘n’,’o’,‘\0’, ‘s’,’o’,’y’,’-‘, ‘c’,’a’,’d’,’e’,’n’,’a’};
Ahora hagamos lo siguiente:
Observamos como la función print RECORTÓ la impresión de otroarreglo[] dado que encontró el carácter Null antes de donde debería ir.
no-soy-cadena
no
Probemos ahora recorrer e imprimir otroarreglo , elemento por elemento ya que es un vector también.
Se observa:
no soy-cadena
De esta manera hemos podido imprimirla dado que lo recorrimos como un vector, como hemos visto.
Veamos lo siguente que pasa cuando la cadena, no es cadena y se usan las funciones de librería de Cadenas
String datos=»soy Juan»; es una cadena
char cadena[]={‘s’,’o’,’y’,’-‘, ‘c’,’a’,’d’,’e’,’n’,’a’,‘\0’}; esta también es cadena
La función concat permite unir cadenas, similar al operador “+” de cadenas también con el Objeto String
Se observa: soy Juansoy-cadena , Unión de Cadenas
Ahora eliminemos el Null ‘\0’ en el fin de la variable cadena y ejecutemos nuevamente:
Se observa: soy Juansoy-cadenasoy Juan , ¿ que pasó aquí?
Sucede que ahora cadena no es mas una cadena ya que perdió su Null y por lo tanto la librería de la función concat concatena pero solo encuentra Null al final de “Soy Juan” que si es cadena y por lo tanto la vuelve a unir. Es muy importante este carácter Null , ya que el grado de problemas que pueden darse por este simple elemento puede ser un tremendo dolor de cabezas, así que a no olvidarse. En forma similar se puede usar el operador “+”, de concatenación.
Conclusión Importante: Si trabajamos los arreglos char[] como cadenas no olvidar el carácter Null ya que si se pretende usar las funciones cadena va a haber problemas “extraños”. Si queremos usar cadenas sin arreglos pueden usar el Objeto String que permite definir variables cadena y automaticamente genera el Null. Las funciones print/println son funciones que tratan a las variables como cadenas de texto
Funciónes de cadena
unacadena.length(): devuelve la longitud de “unacadena” (es decir, el número de caracteres). No se cuenta el carácter nulo que marca los finales de cadena. No tiene parámetros.
unacadena.compareTo(otracadena): compara “unacadena” con otracadena que se pase como parámetro (bien de forma literal, bien mediante un objeto String). Comparar significa detectar qué cadena se ordena antes que la otra. Las cadenas se comparan desde su principio carácter a carácter utilizando el orden de sus códigos ASCII. Esto quiere decir, por ejemplo, que ‘a’ va antes que ‘b’ pero después que ‘A’, y las cifras van antes de las letras. Su valor de retorno será un número negativo si “unacadena” va antes que la cadena pasada como parámetro, 0 si las dos cadenas son iguales o un número positivo si “unacadena” va después que la cadena pasada como parámetro.
Si se quisiera realizar una comparación similar pero entre arrays de caracteres, se puede utilizar la instrucción strcmp(), la cual tiene dos parámetros (que corresponden con los dos arrays a comparar), y cuyo valor de retorno es idéntico a unacadena.compareTo().
unacadena.equals(otracadena): compara si “unacadena” es igual a otracadena, pasada como parámetro. La comparación es “case-sensitive”: esto quiere decir que la cadena “hola” no es igual a “HOLA”. Esta instrucción es equivalente al operador “==” para objetos String . Su valor de retorno es “true” si “unacadena” es igual a la cadena especificada como parámetro, o “false” en caso contrario.
unacadena.equalsIgnoreCase(otracadena): compara si “unacadena” es igual a otracadena, pasada como parámetro. La comparación es “case-insensitive”: esto quiere decir que la cadena “hola” es igual a “HOLA”. Su valor de retorno es “true” si “unacadena” es igual a la cadena especificada como parámetro, o “false” en caso contrario.
unacadena.indexOf(otradacena o carácter,pos): devuelve la posición (un número entero) dentro de “unacadena” donde se encuentra el carácter o el principio de la otracadena especificada como parámetro. Si no se encuentra nada, devuelve -1. Observar que las posiciones se numeran empezando por 0. Por defecto, la búsqueda comienza desde el principio de “unacadena”, pero mediante un segundo parámetro pos opcional se puede indicar la posición a partir de la cual se quiere empezar a buscar. De esta manera, se puede utilizar esta instrucción para encontrar paso a paso todas las ocurrencias que existan de la cadena buscada.
unacadena.lastIndexOf(otracadena/carácter, pos): devuelve la posición (un número entero) dentro de “unacadena” donde se encuentra el carácter o el principio de la otracadena especificada como parámetro. Si no se encuentra nada, devuelve -1. Observar que las posiciones se numeran empezando por 0. Por defecto, la búsqueda comienza desde el final de “unacadena” hacia atrás, pero mediante un segundo parámetro pos opcional se puede indicar la posición a partir de la cual se quiere empezar a buscar (hacia atrás siempre). De esta manera, se puede utilizar esta instrucción para encontrar paso a paso todas las ocurrencias que existan de la cadena buscada.
unacadena.charAt(indice): devuelve el carácter cuya posición indice(dato entero) se haya especificado como parámetro. Las posiciones se numeran empezando por 0.
unacadena.substring(posini,posfin): devuelve la subcadena dentro de “unacadena” existente entre la posición inicial (especificada como primer parámetro) y la posición final (especificada como segundo parámetro opcional). La posición inicial indicada es inclusiva (es decir, el carácter que ocupa esa posición es incluido en la subcadena), pero la posición final –opcional– es exclusiva (es decir, el carácter que ocupa esa posición es el primero en no incluirse en la subcadena). Si dicha posición final no se especifica, la subcadena continúa hasta el final de “unacadena”. Observar que las posiciones se numeran empezando por 0.
unacadena.replace(subcadena / carácter , subcadenareplace): sustituye una subcadena existente dentro de “unacadena” (especificada como primer parámetro) por otra subcadenarepace (especificada como segundo), todas las veces que aparezca. También sirve para sustituir caracteres individuales. La sustitución se realiza sobre “unacadena”, sobrescribiendo su valor original.
unacadena.toLowerCase(): convierte todos los caracteres de “unacadena” en minúsculas. La conversión se realiza sobre “unacadena”, sobrescribiendo su valor original.
unacadena.toUpperCase(): convierte todos los caracteres de “unacadena” en mayúsculas. La conversión se realiza sobre “unacadena”, sobrescribiendo su valor original.
unacadena.trim(): elimina todos los espacios en blanco y tabulaciones existentes al principio y al final de “unacadena”. La conversión se realiza sobre “unacadena”, sobrescribiendo su valor original.
unacadena.concat(otracadena) : añade (“concatena”) al final de la cadena “unacadena” otracadena, pasada como parámetro. Como resultado obtendremos un nuevo valor en “unacadena”: su valor original seguido de ese valor pasado por parámetros, unidos. Es equivalente al operador “+” para objetos String.
unacadena.endsWith(otracadena/caracter): chequea si “unacadena” acaba con los caracteres de otra cadena, pasada por parámetro. Su valor de retorno es “true” si “unacadena” acaba con la cadena especificada como parámetro, o “false” en caso contrario.
unacadena.startsWith(otracadena/ caracter) : Chequea si “unacadena” empieza con los caracteres de otracadena, pasada por parámetro. Su valor de retorno es “true” si “unacadena” empieza con la cadena especificada como parámetro, o “false” en caso contrario.
unacadena.toCharArray(arreglo,cantidad ): copia una cantidad determinada de caracteres pertenecientes a “unacadena” a un array de tipo “char”. Ese arreglo ha de ser especificado como primer parámetro, y la cantidad de caracteres a copiar allí ha de ser especificada como segundo parámetro. Siempre se empiezan a obtener los caracteres desde el principio de “unacadena”. No tiene valor de retorno.
unacadena.getBytes(arreglo, cantidad ): copia una cantidad determinada de caracteres pertenecientes a “unacadena” a un array de tipo “byte”. Ese arreglo ha de ser especificado como primer parámetro, y la cantidad de caracteres a copiar allí ha de ser especificada como segundo parámetro. Siempre se empiezan a obtener los caracteres desde el principio de “unacadena”. No tiene valor de retorno. Hay que tener en cuenta que arreglo [cantidad-1]=Null siempre = Ascii(0), ver más adelante.
unacadena.toInt(): si “unacadena” tiene un valor que empieza por cifras numéricas, esta instrucción es capaz de distinguirlas (descartando los posibles caracteres no numéricos posteriores) y devolver ese valor numérico transformado en un dato de tipo entero. Es decir, transforma una cadena en un número entero, si es posible. Esta instrucción no tiene parámetros.
unacadena.toFloat(): si “unacadena” tiene un valor que empieza por cifras numéricas y punto decimal, esta instrucción es capaz de distinguirlas (descartando los posibles caracteres no numéricos posteriores) y devolver ese valor numérico transformado en un dato de tipo float. Es decir, transforma una cadena en un número float, si es posible. Esta instrucción no tiene parámetros.
unacadena.charAt(pos): Extrae de unacadena el carácter que se encuentra en la posición=pos.
unacadena.setCharAt(pos, caracter): Susbtituye en la unacadena el carácter de la posición=pos por otro carácter especificado en carácter.
Fuente de funciones Arduino Curso práctico de formación de Oscar Torrente Artero , editora AlfaOmega.
A los efectos de ver como funcionan estos métodos adjunto el siguiente programa con algunas de ellas
Recomiendo estas funciones ya que puede hacerse todo lo que necesitemos. Un dispositivo cualquiera, sea monitor de Arduino o cualquier otro que tenga una interface UART 232 TTL, puede enviar datos a Arduino y luego procesarse mediante estas funciones cadena. Serial posee funciones propias que prácticamente hacen lo mismo pero que en la práctica no las he necesitado realmente.
getBytes[] y toCharArray[] merecen análisis separado
Observemos lo que hace la función getBytes(), tomando cantidad = 6, primeros Bytes de la variable cadenaUno del ejercicio anterior. Recordar que cadenaUno=»PanamaHitek»;
La salida es: 80-97-110-97-109-0
80-ASCII de “P”
97-ASCII de “a”
110-ASCII de “n”
97-ASCII de “a”
109- ASCII de “m”
0 – ASCII NulL, como dijimos en la explicación de getBytes(), el elemento [5] = elemento[cantidad-1]= Null
Si quisiéramos tomar en el vector Panamá completo , deberíamos sumar una unidad más a todo:
y la salida sería 80-97-110-97-109-97-Null. Ahora si son los Ascii de todo el substring Panama
El arreglo[cantidad-1]=Nul, no tiene mucho sentido ni razón de porque es esto. Si la conversión fuera no a Bytes[] sino a Char[], tendría sentido porque en realidad obtendríamos una cadena. Quizás sea un “bug” de la versión de Arduino.
Probemos entonces con toCharArray()
Vemos que ahora no es necesario el bucle For como el caso anterior. ¿Por qué? , porque arreglo2[] es un arreglo de caracteres y es uno de los argumentos favoritos de Print y Println.
Por monitor sale: Panamá , lo cual confirma que el elemento arreglo2[6]=Null por lo que automáticamente arreglo2[] se convierte en una CADENA y es por ello que es bien aceptado por print().
Objeto String
Arduino tiene como recurso la variable String, que en realidad es un Objeto ya que si recordamos , el Sketch hereda de C /C++ orientado a objetos. Ya hemos visto lo que hace String, permite definir e inicializar cadenas. Lo más frecuente es utilizar este Objeto para trabajar con Cadenas en forma cómoda.
String cadena=”Hola que tal”; define e inicializa la cadena
String otracadena=”c”; // define e inicializa un carácter como cadena
Si deseamos convertir variable tipo Int, Bytes,long, float,etc a cadenas utilizamos String(variable). Podemos incluso formatear de la siguiente manera.
Cadena=String(20,BIN); toma el entero 20 , lo pasa a binario y lo pasa a cadena. De la misma manera podemos hacer Cadena=String(255,HEX), o Cadena=String(20,OCT).
El operador “+” sirve para concatenar cadenas, como lo hemos visto
String cade=»c»;
String cad=»123″;
String ca=»s.89″;
Serial.println(cade+cad+ca);
Saldrá: c123s.89
En resumen, lo ideal y por experiencia , si vamos a leer datos desde el puerto serial desde el monitor de Arduino o desde cualquier dispositivo Serial asincrónico tipo UART o Serial, lo ideal es leer carácter por carácter y guardarlo en un arreglo buffer[] o cadena String y luego analizarla posteriormente para tratarla. Podemos crear nuestro propio protocolo de comunicación entre Arduino y cualquier dispositivo Serial. Podemos correr una aplicación de Java o C#, por ejemplo donde la interface principal del programa envíe una trama de datos en forma de cadena y al principio y final de la cadena coloquemos uno caracteres especiales de final de trama para que Arduino sepa dónde empieza y donde termina dicha trama y así detectarla con el Serial.read(), ver Aplicación Trasceiver de 433Mhz. Si vamos a leer desde el Monitor de Arduino, recordar que todo lo que envíe se le agrega CR y LF por default, el cual es configurable también como vimos.
Por último, vamos a ver una aplicación donde desde el monitor enviamos un número flotante, por ejemplo el 100.5 y deseamos interpretarlo como lo que es, ya que el monitor va a tomar a dicho número como una cadena de caracteres y enviará los ASCII uno a uno . Luego sumarle otro valor flotante , el mismo 100.5 y presentar el resultado.
El delay(20), como dijimos anteriormente es para asegurar la permanencia del flujo del programa dentro del While(). Existe otra manera que es usar un flag que indique el final de la recepción, y que aunque salte el While() o salga del While() esto no influya:
De esta manera permitimos evitamos el delay( ) del caso anterior que el bloqueante, y el micro no puede hacer otra cosa que esperar. Esto permite que el Micro pueda ejecutar otros procesos.
Si fuese por ejemplo un dispositivo, no desde el monitor , desde donde viniesen los datos, por ejemplo BlueTooth() que también es serie, podríamos inventar una trama en donde la misma termine con el carácter ‘z’. Por ejemplo el dispositivo envía el entero 145z y nosotros tenemos que filtrar y tomar el 145. Suponemos que CR y LF no se usan así que para simular colocamos el monitor en “Sin Ajuste de línea” aunque de la otra manera igual funciona. Para probar este código, desde el monitor haga Send cualquier entero y z, ejemplo 14534z.
Con esto damos por terminada un recorrida de las diversas posibilidades que nos permite trabajar con Cadenas y Arrays[] en general. Espero les sea de utilidad.
Hola!
Muy interesante y útil la información, pero tengo una duda:
Es posible guardar en un array (vacío) los datos que toma un sensor?
Es decir al tomar datos con un sensor ultrasonido y guardarlos en un array para después poder manipularlo?
Gracias! 🙂
Estoy escribiendo un artículo para contestar esta pregunta. Te adelanto algo: LinkedList
Claro que es posible, hay que declarar el array , definir su dimensión estimada y luego con un variable índice que se incremente en cada lectura del sensor se va guardando dicho valor en cada posición