Comunicación avanzada por puerto serie en Arduino


Llevamos una serie de entradas destinadas a hacer un uso avanzado del puerto serie en un procesador como Arduino. En esta entrada nos toca juntar todos estos puntos para hacer, por fin, una comunicación robusta entre dos dispositivos.

En resumen, queremos:

  • Enviar una estructura que contenga nuestro mensaje.
  • Que la comunicación tenga un timeOut para que no sea bloqueante si no hay mensaje que recibir.
  • Disponer de caracteres de control como delimitadores de trama para permitir una sincronización de la comunicación ante la pérdida de paquetes.
  • Disponer de una función de checksum para comprobar la integridad de los datos antes errores de la comunicación.
  • Enviar una señal de Acknowledge.

Si mezclamos todo esto ¿Cómo queda el código en el emisor?

Tenemos una enumeración que indica el estado de la comunicación. También tenemos una estructura DataMessage que contiene el mensaje que queremos enviar, y tenemos definidos los caracteres de control STX, ETC, ACK y NAK.

Anuncio:

Cuando el emisor quiere iniciar una comunicación llama a la función SendMessage, que añade el delimitador inicial, envía la estructura como bytes, calcula y envía el Checksum, y manda el carácter de control delimitador final.

A continuación, espera la respuesta, con un timeout de 100ms. Si recibe ACK, ejecuta la acción okAccion(). Si no recibe respuesta, o recibe NAK, ejecuta la acción errorAction().

const char STX = '\x002';
const char ETX = '\x003';
const char ACK = '\x006';
const char NAK = '\x015';
const int TimeOut = 100;

enum SerialResult
{
  OK,
  ERROR,
  NO_RESPONSE,
};

struct DataMessage
{
   int value;
};
 
struct DataMessage message;

void SendMessage(byte *structurePointer, int structureLength)
{
  Serial.write(STX);
    Serial.write(structurePointer, structureLength);
  
  uint16_t checksum = ChecksumFletcher16(structurePointer, structureLength);
  Serial.write((byte)checksum);
  Serial.write((byte)checksum >> 8);
  Serial.write(STX);
}

uint16_t ChecksumFletcher16(const byte *data, int dataLength)
{
  uint8_t sum1 = 0;
  uint8_t sum2 = 0;

  for (int index = 0; index < dataLength; ++index)
  {
    sum1 = sum1 + data[index];
    sum2 = sum2 + sum1;
  }
  return (sum2 << 8) | sum1;
}

int TryGetACK(int timeOut)
{
  unsigned long startTime = millis();

  while (!Serial.available() && (millis() - startTime) < timeOut)
  {
  }

  if (Serial.available())
  {
    if (Serial.read() == ACK) return OK;
  if (Serial.read() == NAK) return ERROR;
  }
  return NO_RESPONSE;
}


int ProcessACK(const int timeOut, 
       void (*okCallBack)(), 
       void (*errorCallBack)())
{
  int rst = TryGetACK(timeOut);
  if (rst == OK)
  {
    if(okCallBack != nullptr) okCallBack();
  }
  else
  {
    if(errorCallBack != nullptr) errorCallBack();
  }
  return rst;
}

void okAction(byte *data, const uint8_t dataLength)
{
}

void errorAction(byte *data, const uint8_t dataLength)
{
}

void setup()
{
  Serial.begin(9600);
  
  message.value = 10;  

  SendMessage((byte*)&message, sizeof(message));
  ProcessACK(TimeOut, okAction, errorAction);
}

void loop() 
{
}

El código del receptor es similar aunque, como de costumbre, ligeramente más complejo. Tenemos también definida la estructura del mensaje, los códigos de control, y la enumeración del resultado de la comunicación.

El receptor emplea la función ProcessSerialData(), que a su vez llama a la función TryGetSerialData(). Esta función espera la llegada de un mensaje con timeout de 100ms.

Si recibe un mensaje, comprueba que este delimitado por los caracteres de control, recibe la estructura que contiene el mensaje, y comprueba el checksum recibido. Finalmente, devuelve el resultado del intento de comunicación.

Si el mensaje es correcto, la función ProcessSerialData ejecuta la acción okAction(). Si el mensaje es erróneo ejecuta errorAction(). En caso de que expire el timeout no se ejecuta acción alguna.

const char STX = '\x002';
const char ETX = '\x003';
const char ACK = '\x006';
const char NAK = '\x015';
const int TimeOut = 100;

struct DataMessage
{
   int value;
};
 
struct DataMessage message;

enum SerialResult
{
  OK,
  ERROR,
  NO_RESPONSE,
};

uint16_t ChecksumFletcher16(const byte *data, int dataLength)
{
  uint8_t sum1 = 0;
  uint8_t sum2 = 0;

  for (int index = 0; index < dataLength; ++index)
  {
    sum1 = sum1 + data[index];
    sum2 = sum2 + sum1;
  }
  return (sum2 << 8) | sum1;
}

int TryGetSerialData(byte *data, uint8_t dataLength, int timeOut)
{
  unsigned long startTime = millis();

  while (Serial.available() < (dataLength + 4) && (millis() - startTime) < timeOut)
  {
  }

  if (Serial.available() >= dataLength + 4)
  {
    if (Serial.read() == STX)
    {
      for (int i = 0; i < dataLength; i++)
      {
        data[i] = Serial.read();
      }

      if (Serial.read() == ETX && Serial.read() | Serial.read() << 8 == ChecksumFletcher16(data, dataLength))
      {
        return OK;
      }
      return ERROR;
    }
  }
  return NO_RESPONSE;
}

int ProcessSerialData(byte *data, const uint8_t dataLength, const int timeOut, 
       void (*okCallBack)(byte *, const uint8_t ), 
       void (*errorCallBack)(byte *, const uint8_t ))
{
  int rst = TryGetSerialData(data, dataLength, timeOut);
  if (rst == OK)
  {
    Serial.print(ACK);
	if(okCallBack != nullptr) okCallBack(data, dataLength);
  }
  else
  {
	Serial.print(NAK);
	if(errorCallBack != nullptr) errorCallBack(data, dataLength);
  }
  return rst;
}

void okAction(byte *data, const uint8_t dataLength)
{
}

void errorAction(byte *data, const uint8_t dataLength)
{
}

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

void loop()
{
  ProcessSerialData((byte*)&message, sizeof(message), TimeOut, okAction, errorAction);
}

Con estos dos códigos, empezamos a tener una comunicación suficientemente robusta entre dos procesadores por puerto serie.

Librería ComCenter

¿Y si mejoramos este código y lo metemos en una librería para que sea más cómodo de usar? ¡Por supuesto que sí! En una próxima entrada veremos la librería ComCenter para C++ y C#, que realiza esta comunicación por puerto serie de forma fácil y con más funciones.

Si te ha gustado esta entrada y quieres leer más sobre Arduino puedes consultar la sección
tutoriales de Arduino

Descarga el código

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



5 5 votes
Article Rating

Anuncio:

Previous Caracteres de control en el puerto serie en Arduino
Next Comprobar la integridad de datos en Arduino con checksum
12 Comments
oldest
newest
Inline Feedbacks
View all comments
luisllamas
5 years ago

Pues habría que ver el código para darte una respuesta más precisa pero, así a bote pronto, te doy 3 idea – La más común, que será emplear un #define y emplear predirectivas #if. Muy frecuente, muy eficiente, no especialmente limpio (según la programación ‘moderna’) – Otra opción, lo mismo pero con una variable, que inicialices a un valor de 0-3, y en la librería uses un simple if para usar un serial u otro. Más ineficiente que el anterior, e incluso menos limpio que el anterior. – La última opción, que es la que te recomiendo probar, es que… Read more »

Richy
4 years ago

Buenas.
Hordas de personas esperamos ansioso sla explicación de la librería ComCenter.
(y la librería en sí, que yo no la encontré) 🙂

Agustin
4 years ago
Reply to  Luis

Hola Luis. Pudiste publicarla?
Saludos,
Agustin

Richy
3 years ago
Reply to  Luis

Las hordas no nos olvidamos… Seguimos a la espera!

Jose M
4 years ago

Hola Luis, me encanta tu blog!!!

La librería COMCENTER no la encuentro por ningún lado, donde la podría descargar?

Si además hicieras una entrada explicándola, sería genial y te lo agradecería un montón!!!

Muchas gracias!!!

Jose M
4 years ago
Reply to  Luis

Hola Luis, te animo a que la publiques tal y como la tengas y la mejoramos entre todos, qué te parece?

Venga, anímate a publicarla!!!

Muchas gracias y enhorabuena por la web!

Saludos

Jairo
3 years ago

Luis, muchas gracias por toda la info de tu web. Estoy intentando portar el código a Processing (Java), pero no entiendo bien la última comparación de este if;
” if (Serial.read() == ETX && Serial.read() | Serial.read() << 8 == ChecksumFletcher16(data, dataLength))"

Exactamente que comparó después del ETX? que devuelve Serial.read() y Seria.read() << 8 == ChecksumFletcher?

Javito
3 years ago
Reply to  Jairo

buenas.. sino me equivoco.. el If tiene por un lado la condicion
Serial.read()==ETX // lee buffer y verifica que sea fin de trama
&&
(Serial.read() | Serial.read() << 8 )== ChecksumFletcher16(data, dataLength)
(lee los dos byte de la trama que tienen el checksum y los une )==(llama la funcion que calcula el checksum de los datos recividos)

Pepe
3 years ago

Hola Luis.
¿Finalmente vas a publicar la librería ComCenter?
Muchas gracias

Hugo
2 years ago

Hola Luis, Buen día. Finalmente publicaste la librería ComCenter ? Realmente me interesa y espero. Muchas gracias por todo que esta muy bueno. Recibe cordiales saludos