It’s been over three years since we presented the basic tutorial on the serial port as one of the main ways to communicate with a processor like Arduino. Since then, it has been a common component in the example codes on the blog.
By now, we are ready (and it was about time) to delve deeper into the subject. For that, we are going to dedicate a series of advanced tutorials to see how to receive text strings, numbers, arrays, comma-separated files, and it will end by sending a series of bytes and presenting the ComCenter library for robust serial port communication in Arduino.
In this first entry, we are going to see how to handle characters and text strings. If you search the internet, you will see a great variety of codes for receiving text. Some are good, some are passable, and others… quite horrible.
The reason is that there is more than one way to deal with text in the serial port, each with its advantages and disadvantages. Here we are going to see the main ways to receive characters and text, emphasizing when it is convenient to use one or the other.
The DEBUG functions are only for visualizing the results. When you use this code, you can remove the parts related to its definition and use.
Read a character
The first case we are going to see is reading a single character through the serial port. The code is very simple, as we only need to read a byte and convert it to a char. Despite being simple, it can be useful, for example, to accept commands that we send to a robot or a vehicle.
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
Serial.setTimeout(50);
}
void loop()
{
if (Serial.available())
{
char data = Serial.read();
if ((data >= 'A' && data <= 'Z') || (data >= 'a' && data <= 'z'))
{
DEBUG((char)data);
}
}
}
Having practically no processing, the speed is the highest of all the examples we are going to see (of the order of 4-8us). The disadvantage is, logically, that we are only receiving a single character.
Also, being a manual process, it is easy to add validation conditions. In the example, we accept characters from A to Z in uppercase or lowercase, but it would be easy, for example, to accept only values from A to D because those are the four commands our assembly expects.
Read a text string with the String class
If we want to read a complete text string, we can use the String class, which, as we know, is a wrapper around a char array included in the Arduino IDE, and provides functions to work comfortably with text strings.
Combined with the Serial.readStringUntil(char) function, we can read a text string received by the serial port comfortably and easily. For example, it is common to use ‘\n’ as a separator to receive a complete line.
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
}
void loop()
{
if (Serial.available())
{
String data = Serial.readStringUntil('\n');
DEBUG(data);
}
}
The method is considerably efficient, although, of course, much slower than receiving a single char. The speed depends on the processor and clock speed, but as a rough guide, it is of the order of 1ms per received character.
Read text string with char array
Another alternative is to use a char array instead of the String class to receive a text string. The operation is similar, but we define a buffer before receiving the string, instead of an instance of String.
This time we use the Serial.readBytesUntil(char, char*, int) function, which receives a text string through the serial port and stores it in the buffer. The function returns the number of characters received and, therefore, occupied in the buffer.
#define DEBUG(a, b) for (int index = 0; index < b; index++) Serial.print(a[index]); Serial.println();
void setup()
{
Serial.begin(9600);
Serial.setTimeout(50);
}
void loop()
{
if (Serial.available())
{
char data[20];
size_t count = Serial.readBytesUntil('\n', data, 20);
DEBUG(data, count)
}
}
The performance is very similar to using the functions of the String class. It is slightly higher, as the amount of processing is lower, at the cost of dispensing with the dynamic size functions of the String class.
As disadvantages, it is less convenient to use and, above all, we have to define the size of the buffer before receiving the string. If we fall short, we will receive the string incorrectly, and if we exceed, we will be wasting memory unnecessarily.
Therefore, in general, we will prefer to use the functions of the String class, unless specifically we do not want or cannot use the String class in our project.
Read string with Naive method
The last example we are going to see is the “naive method”, which, as we know, is an elegant way of saying to do the process “by hand”. We are going to see it in two different variants, using the String class or a char array.
The first way is to use the String class to perform the concatenation. We simply receive a char, and if it is different from the separator, we use the String.concat() function to perform the concatenation.
#define DEBUG(a) Serial.println(a);
String data = "";
void setup()
{
Serial.begin(9600);
}
void loop()
{
while (Serial.available())
{
char character = Serial.read();
if (character != '\n')
{
data.concat(character);
}
else
{
Serial.println(data);
data = "";
}
}
}
The advantage of the method is to provide us with total control over the process. The efficiency of the method is similar to those shown previously, as the implementation is similar to the one used internally by the functions.
The biggest disadvantage is that it represents a greater hassle. Notice, for example, that we had to define the String variable as global, or otherwise any wait in the main loop would cause it to restart.
If, again, we do not want or cannot use the String class, we can also use a char array to receive the text string. We simply define a buffer and increment the index when performing concatenation.
#define DEBUG(a) Serial.println(a);
char data[20];
size_t dataIndex = 0;
void setup()
{
Serial.begin(9600);
}
void loop()
{
while (Serial.available())
{
char character = Serial.read();
if (character != '\n')
{
data[dataIndex] = character;
dataIndex ++;
}
else
{
Serial.println(data);
dataIndex = 0;
}
}
}
Logically, we have all the disadvantages of the naive method in terms of ease of use, plus all the problems associated with using a fixed-size char array that we have seen previously.
Therefore, it is normal for us to avoid using this method compared to those presented earlier, unless specifically we have reasons in our project.
Download the code
All the code in this post is available for download on Github.