Language: EN

puerto-serie-avanzado-arduino

Advanced Serial Communication in Arduino

We have a series of entries aimed at making advanced use of the serial port in a processor like Arduino. In this post, it’s time to put together all these points to finally make robust communication between two devices.

In summary, we want to:

  • Send a structure containing our message.
  • Have a timeOut for communication so it’s non-blocking if there’s no message to receive.
  • Have control characters as frame delimiters to allow communication synchronization in case of packet loss.
  • Have a checksum function to check the integrity of the data before communication errors.
  • Send an Acknowledge signal.

If we mix all of this, what does the code look like on the sender side?

We have an enumeration that indicates the communication state. We also have a DataMessage structure that contains the message we want to send, and we have defined the control characters STX, ETC, ACK, and NAK.

When the sender wants to initiate communication, it calls the SendMessage function, which adds the initial delimiter, sends the structure as bytes, calculates and sends the Checksum, and sends the final delimiter control character.

Then, it waits for the response, with a timeout of 100ms. If it receives ACK, it executes the okAction(). If it receives no response, or receives NAK, it executes the 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() 
{
}

The code for the receiver is similar, although slightly more complex as usual. We also have the message structure defined, control codes, and the communication result enumeration.

The receiver uses the ProcessSerialData() function, which in turn calls the TryGetSerialData() function. This function waits for a message to arrive with a timeout of 100ms.

If it receives a message, it checks that it is delimited by the control characters, receives the structure containing the message, and checks the received checksum. Finally, it returns the result of the communication attempt.

If the message is correct, the ProcessSerialData function executes the okAction(). If the message is incorrect, it executes errorAction(). If the timeout expires, no action is taken.

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);
}

With these two codes, we begin to have sufficiently robust communication between two processors via serial port.

ComCenter Library

What if we improve this code and put it into a library to make it more convenient to use? Of course, we can! In a future entry, we will see the ComCenter library for C++ and C#, which performs this serial port communication easily and with more functions.

Download the Code

All the code from this post is available for download on Github. github-full