We have a series of posts aimed at making advanced use of the serial port in a processor like Arduino. In this post, we will combine all these points to finally create robust communication between two devices.
In summary, we want:
- To send a structure containing our message.
- The communication to have a timeout so it is non-blocking if there is no message to receive.
- To have control characters as frame delimiters to allow communication synchronization in case of packet loss.
- To have a checksum function to verify data integrity against communication errors.
- To send an Acknowledge signal.
If we mix all this, what does the code look like on the transmitter side?
We have an enumeration indicating the communication status. We also have a DataMessage structure that contains the message we want to send, and we have defined the control characters STX, ETX, ACK, and NAK.
When the transmitter 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 receiver code is similar, though, as usual, slightly more complex. We also have the message structure defined, the control codes, and the enumeration for the communication result.
The receiver uses the ProcessSerialData() function, which in turn calls the TryGetSerialData() function. This function waits for a message to arrive with a 100ms timeout.
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 erroneous, it executes errorAction(). If the timeout expires, no action is executed.
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 start 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! In a future post, 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.

