arduino-bytes-puerto-serie

Sending or Receiving Bytes through Serial Port in Arduino

  • 5 min

In this post, we will see how to send and receive messages via serial port by performing communication directly with bytes.

In recent posts, we have seen methods for receiving numbers and text strings on a microprocessor like Arduino. We have even seen how to receive arrays separated by commas or another separator (although it’s usually not a good idea).

However, we had already mentioned that at advanced levels, it is normal to work directly with byte transmissions. And that’s right, the big boys/girls send bytes.

Is it more complex to communicate with bytes? Not at all. It’s even simpler than direct text handling. But yes, we need to understand what we are doing.

In general, when establishing communication where we control both the receiver and the transmitter, the first thing we will do is define a frame, that is, a specific sequence of bytes in an order that transmits a message with a determined structure.

In this post, we will see how to send and receive byte sequences, and how to generate and handle these byte sequences from our variables (int, float, etc.).

In upcoming posts, we will see how to send an array of numbers (without needing to use a comma-separated array), and in the next one, sending arbitrary groupings of variables using a structure.

Send a Series of Bytes

Sending a byte sequence is simple since the Serial library provides the write(byte*, int) function that performs exactly this function.

const byte data[] = {0, 50, 100, 150, 200, 250};
const size_t dataLength = sizeof(data) / sizeof(data[0]);

void setup()
{
  Serial.begin(9600);
  Serial.write(data, dataLength);
}

void loop() 
{
}
Copied!

Receive a Series of Bytes

Receiving a sequence is not much more complicated, as we also have the read(byte*, int) function.

However, integrating it into our code is a bit more complex because, unlike the transmitter which is an active agent (it knows when it wants to send), the receiver is a passive subject (it must check if a message is available while continuing to execute its control loop, with a greater or lesser degree of asynchronicity).

For example, the following code uses a TryGetSerialData() function that we call from the main loop. If the number of bytes available is greater than the number of bytes expected, the reading is performed and it returns true. Otherwise, it returns false.

The control loop uses the returned value to perform the appropriate action when a data packet is received, represented in the example by the OkAction() function.

const int NUM_BYTES = 3;
byte data[NUM_BYTES];

bool TryGetSerialData(const byte* data, uint8_t dataLength)
{
  if (Serial.available() >= dataLength)
  {
    Serial.readBytes(data, dataLength);
    return true;
  }
  return false;
}

void OkAction()
{
}

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

void loop()
{
  if(TryGetSerialData(data, NUM_BYTES))
  {
    OkAction();
  }
}
Copied!

What Do We Do with the Received Bytes?

Great, we now know how to send and receive byte sequences. But how do we generate these packets from our variables? And how do we convert them back into variables when we have received them? Here comes the somewhat “annoying” part, which has been a problem in computing since the world began (well, at least the computing world).

Depending on the architecture of the processor we are using, the different variables are represented by a different number of bytes. For example, on one processor an int variable might have 16 bits, and on another 32 bits. This small difference can cause many headaches, although we can easily control it as long as we keep this point in mind.

For this reason, it is advisable to use more specific variables like uint8_t, uint16_t, which unambiguously indicate the number of bits used to store the variable.

In Arduino UNO, Nano, and Mega, int is equivalent to uint16_t and long to uint32_t.

To make sending easier, we can create functions to send 1, 2, and 4 bytes.

void sendBytes(uint8_t value)
{
  Serial.write(value);
}

void sendBytes(uint16_t value)
{
  Serial.write(highByte(value));
  Serial.write(lowByte(value));
}

void sendBytes(uint32_t value)
{
  int temp = value & 0xFFFF;
  sendBytes(temp);
  temp = value >> 16;
  sendBytes(temp);
}
Copied!

And here we have the equivalent functions to convert groups of bytes into variables of 1, 2, and 4 bytes respectively.

uint8_t byteToInt(byte byte1)
{
  return (uint8_t)byte1;
}

uint16_t byteToInt(byte byte1, byte byte2)
{
  return (uint16_t)byte1 << 8 | (uint16_t)byte2;
}

uint32_t byteToLong(byte byte1, byte byte2, byte byte3, byte byte4)
{
  return byte1 << 24 
           | (uint32_t)byte2 << 16
           | (uint32_t)byte3 << 8
           | (uint32_t)byte4;
}
Copied!

However, there are faster and more convenient ways to perform these conversions, such as direct casting. For this, it will be necessary that we have a good understanding that the size of the variables is agreed upon between the transmitter and receiver. Agreed means that it does not need to be identical, but then one of the agents (the transmitter or the receiver) will have to perform the conversion accordingly.

In future posts, we will delve deeper into these aspects.

Download the Code

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