cadenas-de-texto-puerto-serie-arduino

Enviar y recibir textos por puerto serie en Arduino

Hace más de tres años que presentamos el tutorial básico sobre el puerto serie como una de las principales formas de comunicar con un procesador como Arduino. Desde entonces, ha sido un componente habitual en los códigos de ejemplo del blog.

A estas alturas ya estamos preparados (y además, ya iba siendo hora) para profundizar en el tema. Para eso vamos a dedicarle una serie de tutoriales avanzados donde ver cómo recibir cadenas de texto, números, arrays, ficheros separados por coma, y que terminará enviando serie de bytes y la presentación de la librería ComCenter para una comunicación robusta por puerto serie en Arduino.

En esta primera entrada vamos a ver cómo tratar caracteres y cadenas de texto. Si buscamos con internet veréis una gran variedad de códigos para recibir texto. Algunos son buenos, otros pasables y otros… francamente bastante horribles.

El motivo es que existe más de una forma de tratar con texto en puerto serie, cada una con sus ventajas y desventajas. Aquí vamos a verlas principales formas de recibir caracteres y texto, haciendo hincapié en cuando conviene usar una u otra.

La funciones DEBUG están únicamente para visualizar los resultados, cuando uséis este código podéis quitar las partes relativas a su definición y uso.

Leer un carácter

El primer caso que vamos a ver es leer un único carácter por puerto serie. El código es muy sencillo, ya que únicamente necesitamos leer un byte y convertirlo a char. Pese a ser sencillo puede ser útil, por ejemplo, para aceptar comandos que enviamos a un robot o un vehículo.


#define DEBUG(a) Serial.println(a);

void setup()
{
   Serial.begin(9600);
   Serial.setTimeout(50);
}

void loop()
{
   if (Serial.available())
   {
      char data = Serial.read();
      if ((data >= 'A'  && data <= 'Z') || (data >= 'a' && data <= 'z'))
      {
         DEBUG((char)data);
      }
   }
}

Al carecer prácticamente de procesamiento, la velocidad es la mayor de todos los ejemplos que vamos a ver (del orden de 4-8us). La desventaja es, lógicamente, que sólo estamos recibiendo un único carácter.

Además al ser un proceso manual es sencillo añadir condiciones de validación. En el ejemplo aceptamos caracteres de A a Z en mayúsculas o minúsculas pero sería sencillo, por ejemplo, aceptar únicamente valores de la A a D porque son los cuatro comandos que espera nuestro montaje.

Leer una cadena de texto con clase String

Si queremos leer una cadena de texto completa podemos emplear la clase String, que como sabemos es un wrapper alrededor de un char array incluido en el IDE de Arduino, y que proporciona funciones para trabajar cómodamente con cadenas de texto.

Combinado con la función Serial.readStringUntil(char), podemos leer una cadena de texto recibida por puerto serie de forma cómoda y sencilla. Por ejemplo, es habitual usar como separador ‘\n’, para recibir una línea completa.


#define DEBUG(a) Serial.println(a);

void setup()
{
   Serial.begin(9600);
}

void loop()
{
   if (Serial.available())
   {
      String data = Serial.readStringUntil('\n');
      DEBUG(data);
   }
}

El método es considerablemente eficiente aunque, por supuesto, mucho más lento que recibir un único char. La velocidad depende del procesador y la velocidad de reloj, pero a modo orientativo es del orden de 1ms por caracter recibido.

Leer cadena de texto con char array

Otra alternativa es emplear un char array en lugar de la clase String para recibir una cadena de texto. El funcionamiento es similar, pero definimos un buffer antes de recibir la cadena, en lugar de una instancia de String.

En esta ocasión empleamos la función Serial.readBytesUntil(char, char*, int), que recibe una cadena de texto por puerto serie y la almacena en el buffer. La función devuelve el número de caracteres recibidos y, por tanto, ocupados en el buffer.


#define DEBUG(a, b) for (int index = 0; index < b; index++) Serial.print(a[index]); Serial.println();

void setup()
{
   Serial.begin(9600);
   Serial.setTimeout(50);
}

void loop()
{
   if (Serial.available())
   {
      char data[20];
      size_t count = Serial.readBytesUntil('\n', data, 20);
      DEBUG(data, count)
   }
}

El rendimiento es muy similar a usar las funciones de la clase String. Es levemente superior, ya que la cantidad de procesamiento es inferior, a costa de prescindir de las funciones de tamaño dinámico de la clase String.

Como desventajas tenemos que es menos cómodo de usar y, sobre todo, que tenemos que definir el tamaño del buffer antes de recibir la cadena. Si nos quedamos corto, recibiremos mal la cadena, y si nos excedemos estaremos desperdiciando memoria de forma innecesaria.

Por tanto, en general, preferiremos emplear las funciones de la clase String, salvo que específicamente no queramos o no podamos usar la clase String en nuestro proyecto.

Leer cadena con método Naive

El último ejemplo que vamos a ver es el “método naive”, que como sabemos es una forma elegante de decir hacer el proceso “a mano”. Vamos a verlo en dos variantes distintas, usando la clase String o un char array.

La primera forma es usar la clase String para realizar la concatenación. Simplemente recibimos un char, y si es distinto del separador empleamos la funcion String.concat() para realizar la concatenación.


#define DEBUG(a) Serial.println(a);

String data = "";

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  while (Serial.available())
  {
    char character = Serial.read();
    if (character != '\n')
    {
      data.concat(character);
    }
    else
    {
      Serial.println(data);
      data = "";
    }
  }
}

La ventaja del método es proporcionarnos un control total sobre el proceso. La eficiencia del método es similar a los mostrados con anterioridad, ya que la implementación es similar a la empleada interiormente por las funciones.

La mayor desventaja es que representa un mayor engorro. Fijaros, por ejemplo, que hemos tenido que definir la variable String como global o de lo contrario cualquier espera en el bucle principal haría que se reiniciara.

Si nuevamente, no queremos o no podemos usar la clase String, igualmente podemos emplear un char array para recibir la cadena de texto. Simplemente definimos un buffer e incrementamos el indice al realizar la concatenación.


#define DEBUG(a) Serial.println(a);

char data[20];
size_t dataIndex = 0;

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  while (Serial.available())
  {
    char character = Serial.read();
    if (character != '\n')
    {
      data[dataIndex] = character;
      dataindex ++;
    }
    else
    {
      Serial.println(data);
      dataIndex = 0;
    }
  }
}

Lógicamente, tenemos todas las desventajas del método naive en cuanto a incomodidad de uso, más todos los problemas asociados a usar un char array de tamaño fijo que hemos visto con anterioridad.

Por tanto, lo normal es que evitemos usar este método frente a los presentados anteriormente salvo que específicamente tengamos motivos en nuestro proyecto.

Descarga el código

Todo el código de esta entrada está disponible para su descarga en Github. github-full