Comunicación serial con Arduino

Autor: José Olin
Publicado: 2022-01-11

Has terminado tus primeros proyectos con Arduino, como encender y apagar un LED o controlar un motor de DC y te preguntas ¿cómo podrías controlarlo de forma remota? Aunque hay varios protocolos de comunicación para Arduino como SPI o i2c, el más usual es el protocolo UART.

En este post veremos cómo hacer la comunicación serial con Arduino desde el computador, desde un caracter hasta una cadena con varias variables que deben procesarse en el Arduino.

Si te interesa ver información introductoria acerca del protocolo UART y Arduino puedes leer Comunicación de Arduino con puerto serie, de Luis Llamas. Si te interesa profundizar en los pormenores del protocolo UART y otros protocolos seriales puedes revisar Arduino Communication Peripherals: UART, I2C and SPI, y UART vs I2C vs SPI – Communication Protocols and Uses

Usemos como base el viejo y confiable código para

Encender y apagar un LED.

int pinLED = 13;

void setup()
{
    pinMode(pinLED, OUTPUT);
}

void loop()
{
    digitalWrite(pinLED, HIGH);
    delay(500);
    digitalWrite(pinLED, LOW);
    delay(500);
}

¿Cómo podríamos encender y apagar el LED a voluntad desde el computador? Ahí es donde entra la comunicación serial, que usaremos con la librería Serial de Arduino.

Encender y apagar el LED desde la computadora

Prueba el siguiente código.

int pinLED = 13;

void setup()
{
    Serial.begin(115200);
    Serial.println("Com iniciada");
    pinMode(pinLED, OUTPUT);
}

void loop()
{
    if(Serial.available() > 0)
    {
        char car = Serial.read();
        Serial.print("Caracter recibido: ");
        Serial.println(car);

        if(car == 'e')
        {
            digitalWrite(pinLED, HIGH);
        }
        if(car == 'a')
        {
            digitalWrite(pinLED, LOW);
        }

    }
}

Todas las líneas referentes a comunicación inician con Serial, la librería que se encarga de la magia.

Entre las líneas agregadas cabe destacar:

Serial.begin(115200);

Inicializa la comunicación serial a una velocidad de 115200 bits por segundo.

Serial.println("Com iniciada");
Serial.print("Caracter recibido: ");

Envían un mensaje al monitor serial (Herramientas -> Monitor Serial, para verlo). Println envío un salto de línea después del mensaje, mientras que print deja el cursor al final del mensaje, permitiendo concatenar otros valores.

Serial.available()

Devuelve la cantidad de caracteres que hay disponibles para leer en el buffer; un valor mayor a cero indica que hay mensaje disponible.

Serial.read()

Lee el siguiente caracter (y sólo uno) del mensaje disponible.

Si ahora envías el caracter e desde el monitor Serial de la computadora, el LED del Arduino conectado al pin digital 13 debe encender, y cuando envíes a se debe apagar.

Enviar valores enteros (0 a 9)

"Eso está muy bien, pero qué tal si en lugar de un caracter queremos enviar un valor que controle la intensidad de un LED o la velocidad de un motor".

Buena pregunta, mi querido Padawan.

Esto requiere cambiar el código para validar que el byte recibido corresponde a un número (al código ASCII de un número) y por otro lado usar un pin que soporte PWM, por ejemplo el 9 (los pines que soportan PWM tienen una ~ a un lado).

int pinLED = 9;

void setup()
{
    Serial.begin(115200);
    Serial.println("Com iniciada");
    pinMode(pinLED, OUTPUT);
}

void loop()
{
    if(Serial.available() > 0)
    {
      char car = Serial.read();
      Serial.print("Caracter recibido: "); 
      Serial.println(car);
      if(car >= '0' && car <= '9')
      {
        int num = car - '0'; //Convertir caracter a entero.
        Serial.print("int: "); Serial.println(num);
        int valorPWM = num * 25;
        analogWrite(pinLED, valorPWM);
      }
      else
      {
        Serial.println("Solo numeros, joven");
      }

    }
}

La línea:

if(car >= '0' && car <= '9')

Valida que el caracter recibido corresponda al código ASCII de un número, y si es el caso, recupera su valor entero y genera un valor de PWM adecuado para la función analogWrite (entre 0 y 255).

Si ahora envíamos un valor entre 0 y 9 desde el monitor serial debería encender el LED que conectamos al pin 9 (lo conectaste con una resistencia de al menos 220 Ohms, ¿cierto?) debería encender con mayor o menor intensidad.

Te dejamos una versión equivalente y más ordenada del código, que implementa la lectura en una función llamada leerEntero.

int pinLED = 9;

int leerEntero()
{
    /* Función que hace la lectura y validación */
   int valor = -1;
   if(Serial.available() > 0)
   {
      char car = Serial.read();
      if(car >= '0' && car <= '9')
      {
        int num = car - '0'; //Convertir caracter a entero.
        valor = num;
      }
   }
   return valor;
}

void setup()
{
    Serial.begin(115200);
    Serial.println("Com iniciada");
    pinMode(pinLED, OUTPUT);
}

void loop()
{

    int value = leerEntero();

    if(value >= 0)
    { 
      int valuePWM = value * 25;
      analogWrite(pinLED, valuePWM);
    }
}

"Muchas gracias por esa función, el código se ve realmente ordenado y agradable"

Con gusto, mi querido Padawan.

Leer un entero de más de un dígito

"Lo anterior está muy bien, pero ¿qué tal si mi programa requiere un entero de más de un dígito para realizar el control"

Veo que tienes hambre de conocimiento, y me hace sentir orgulloso.

En ese caso podemos usar la función parseInt de la librería Serial.

/*
Considerar que parseInt y parseFloat heredan de Stream por lo que son funciones con bloqueo.
*/

int pinLED = 9; // O cualquier pin que soporte PWM ( ~ )

long leerEntero_conBloqueo()
{
    long valor = 0; // Es necesario inicializar, de lo contrario toma valores     
                        //aleatorios (basura en la mem) y devolvera gralte algo dif de 0.

   while (Serial.available() > 0 )
   {
      valor = Serial.parseInt();
      // Tambien esta disponible la funcion parseFloat.
   }
   return valor;
}

void setup()
{
    Serial.begin(115200);
    Serial.println("Com iniciada");
    //delay(1000);
    pinMode(pinLED, OUTPUT);
}

void loop()
{
    long value = leerEntero_conBloqueo();

    if(value != 0)
    {
      // Si solo hay caracteres no numericos parseInt devuelve 0..
      Serial.print("Valor recibido: ");
      Serial.println(value);
    }


    int valuePWM;
    if(value > 0 && value <= 255)
    { 
      valuePWM = value;
    }
    else
    {
      valuePWM = 0;
      //Serial.println("Valor fuera del rango, joven");
    }
    analogWrite(pinLED, valuePWM);
}

En realidad la función puede recibir cualquier valor entre -2,147,483,648 y 2,147,483,647 que es lo que puede almacenar una variable long de Arduino, pero lo limitamos entre 0 y 255 para nuestra aplicación.

El único inconveniente, es que la función parseInt es una función con bloqueo, esto es, que se quedará en espera por algún tiempo hasta recibir el entero que le prometiste, lo cual podría no ser conveniente en aplicaciones de tiempo real, como el sistema Isiukak.

Leer un entero de más de un dígito sin bloqueo

La idea para recibir no sólo enteros, sino prácticamente cualquier conjunto de datos vino de Serial Input Basics - updated, una lectura completamente recomendada.

/*
 Este ejemplo asume que enviaste algo como "<HelloWorld, 12, 24.7>".
 */


// Example 5 - Receive with start- and end-markers combined with parsing

const byte numChars = 32;
char receivedChars[numChars];
char tempChars[numChars];        // temporary array for use when parsing

// variables to hold the parsed data
char messageFromPC[numChars] = {
  0};
int integerFromPC = 0;
float floatFromPC = 0.0;

boolean newData = false;

//============

void setup() 
{
  Serial.begin(115200);
  Serial.println("This demo expects 3 pieces of data: text, an integer and a floating point value");
  Serial.println("Enter data in this style <HelloWorld, 12, 24.7>  ");
  Serial.println();
}

//============

void loop() 
{
  recvWithStartEndMarkers();

  if (newData == true) 
  {
    strcpy(tempChars, receivedChars);
    // this temporary copy is necessary to protect the original data
    //   because strtok() used in parseData() replaces the commas with \0
    parseData();
    showParsedData();
    newData = false;
  }
}

//============

void recvWithStartEndMarkers() 
{
  static boolean recvInProgress = false;
  static byte ndx = 0;
  char startMarker = '<';
  char endMarker = '>';
  char rc;

  while (Serial.available() > 0 && newData == false) 
  {
    rc = Serial.read();

    if (recvInProgress == true) {
      if (rc != endMarker) {
        receivedChars[ndx] = rc;
        ndx++;
        if (ndx >= numChars) {
          ndx = numChars - 1;
        }
      }
      else {
        receivedChars[ndx] = '\0'; // terminate the string
        recvInProgress = false;
        ndx = 0;
        newData = true;
      }
    }

    else if (rc == startMarker) {
      recvInProgress = true;
    }
  }
}

//============

void parseData() {      // split the data into its parts

  char * strtokIndx; // this is used by strtok() as an index

    strtokIndx = strtok(tempChars,",");      // get the first part - the string
  strcpy(messageFromPC, strtokIndx); // copy it to messageFromPC

  strtokIndx = strtok(NULL, ","); // this continues where the previous call left off
  integerFromPC = atoi(strtokIndx);     // convert this part to an integer

  strtokIndx = strtok(NULL, ",");
  floatFromPC = atof(strtokIndx);     // convert this part to a float

}

//============

void showParsedData() {
  Serial.print("Message: ");
  Serial.println(messageFromPC);
  Serial.print("Integer: ");
  Serial.println(integerFromPC);
  Serial.print("Float: ");
  Serial.println(floatFromPC);
}

La función recvWithStartEndMarkers leerá un caracter en cada ciclo del loop y cuando el mensaje se haya recibido completo, invocará a parseData para separar las variables en el mensaje.



.
Esta publicación es posible gracias a nuestros Patreons.