puerto-serie-avanzado-arduino

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.

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.

Descarga el código

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