Language: EN

arduino-bytes-puerto-serie

Sending or Receiving Bytes through Serial Port in Arduino

In this post, we will see how to send and receive messages through the serial port by directly communicating with bytes.

In the last posts, we have seen methods to receive numbers and text strings in a microprocessor like Arduino. We have even seen how to receive arrays separated by commas or another separator (although it is usually not a good idea).

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

Is it more complex to communicate with bytes? No, not at all. It is even simpler than direct text processing. But yes, we have to understand what we are doing.

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

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

In future posts, we will see how to send an array of numbers (without the need to use an array separated by commas), and in the next one, the sending of arbitrary variable groupings using a structure.

Sending a series of bytes

Sending a sequence of bytes is simple since the Serial library provides the write(byte*, int) function that performs 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() 
{
}

Receiving a series of bytes

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

However, integrating it into our code is somewhat more complex since, unlike the transmitter which is an active agent (knows the moment 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 greater or lesser degree of asynchrony).

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 expected number of bytes, the reading is performed and true is returned. Otherwise, false is returned.

The control loop uses the returned value to take the appropriate action when a data packet is received, in the example represented 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();
  }
}

What do we do with the received bytes?

Perfect, we already 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 can have 16 bits, and on another, 32 bits. This small difference can cause a lot of headaches, although we can easily control it as long as we keep this point in mind.

For this reason, it is recommended to use more specific variables such as uint8_t, uint16_t, which unequivocally 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 uint36_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);
}

And here are the equivalent functions to convert groups of bytes to 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;
}

However, there are faster and more convenient ways to perform these conversions, such as direct casting. For this, it will be necessary to control well that the size of the variables is agreed upon between the transmitter and the receiver. Agreed upon means that it is not necessary to be identical, but then one of the agents (the transmitter or the receiver) will have to make 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