Introduction
When writing Arduino programs, you sometimes need to receive some commands or data from serial line. It may seem easy but often you run into problems. Let me explain what I mean by an example. You have a program which is doing lot of things – reads the sensors, controls outputs, shows current status on a display, etc. You make your loop code run fast, for example, it is executed 10 times per second. Now you want your program to respond to commands sent from the serial line or to process data sent from another Arduino. How can you do this?
Well, you put if (Serial.available() > 0)
into your loop and if there are some data (or command), you process it. But here, the problems start to show up. The condition is true if there are one or more characters available. If your data or command is more than 1 char long, there is a good chance that you cannot process it yet because it is not completely received. There may be just the first char, or the first two chars, etc. One solution would be to wait for all data to come; something like calling Serial.readStringUntil
which will read until a terminator character is received or there is a timeout. But you cannot stop your loop and wait! You want to keep processing the inputs and controlling outputs 10 times per second – imagine your program controls a quadrocopter; it cannot just stop controlling the aircraft and wait for the data to arrive from serial line.
Another solution is to change the condition to something like this:
if ( Serial.available() == 3 )
Process the data….
In this code, I assume that the data/command are 3 chars long and I only do the processing if there are exactly 3 characters received. Seems fine? Not always. Let me show you an example:
Suppose one Arduino is sending numbers (fixed 3 digits length) once per second. Our program in another Arduino is receiving the numbers and it needs to run its loop 10 times per seconds to control the nuclear reactor in your basement. Here is what the sequence of char
s coming from the serial line could look like:
001 002 003 004.
Now if the receiving program is “in sync” – if it starts reading from the beginning, it will receive the numbers all right. But imagine, for example, that the receiving Arduino is powered off and then back on. It may start receiving at any random position in the sequence. For example, the first char
it receives may be “2
”, and then the condition if (Serial.available() == 3)
will be true
when we receive "200
" instead of "002
" or "003
" – invalid number.
The situation may be even worse if the sender is a person rather than another Arduino. There may be invalid commands, random spaces between each char
, etc. Plus you probably don’t want to limit the commands to fixed length. So this solution is also far from ideal.
Note: The Serial Monitor in Arduino IDE may somehow hide these problems because it lets the user type the whole string
(command) and then send it by the Send button. So the data on the serial line come in short bursts like “command1 command2
” instead of “c o m m a n d 1 c o m
“ etc., which will happen if the chars are sent immediately after the user types them – which is what normal terminal programs do. Anyway, I am sure you want your program to be robust and handle all the situations nicely.
So here, I present a solution which should be able to handle the characters coming in random times and process the commands or data when it is really ready without blocking the loop for long time.
Using the Code
The first example processes commands from the user. The commands can be up to MAX_DATA_LEN
char
s long and they are received in a way which does not block the loop()
function. The code doesn't make any assumptions such as that the program will be always receiving characters when the user starts to type.
Important Note: When you test the program in Arduino Serial Monitor, set it to send the Carriage Return characted in the bottom of the window instead of the default No line ending. The program expects the commands to be terminated by Return key (carriage return char).
About String Class
In the code, I use C language string
s - that is an array of char
s. There is no data type for string
s in the C language. It is possible to use string
class in C++ and there is a String
class in Arduino which allows you to work with string
s the way you may know from languages like C# or Java, but I don’t recommend using this class. For one, I’ve read some bad news about its efficiency, memory leaks, etc., and for second even if the class was well written, it will be still quite inefficient compared to the C language string
handling because it will need to dynamically allocate and free memory when you do things like myString = myString + “ you too!”;
Using dynamic memory and sometimes even C++ is not such a good idea on embedded systems like Arduino, as I tried to explain in my article here.
I know that working with C style string
s looks scary, but it is in fact rather easy and once you learn it, you will be able to write efficient code and do all the cool tricks with the characters you receive.
What the Program Does
The example code below receives commands from serial line and executes them. These commands are supported:
ledon
to turn on the onboard LED on pin 13 ledoff
to turn off the onboard LED ver
to print the verison of the program.
In the loop()
, there is delay(100);
which simulates some useful work the program should do, such as reading sensors, processing the inputs, etc. Then there is function processSerialData
which takes care of processing the commands received from serial line. It does not block the program for long, just stores the character(s) received since it was last called and if a complete command is received, it will execute it. The code is explained in more detail below:
#include <string.h> // for strcmp()
#define MAX_DATA_LEN (8)
#define TERMINATOR_CHAR ('\r')
char g_buffer[MAX_DATA_LEN + 1];
void processSerialData(void);
void setup() {
pinMode(13, OUTPUT);
Serial.begin(9600);
}
void loop() {
delay(100);
processSerialData();
}
void processSerialData(void) {
int data;
bool dataReady;
while ( Serial.available() > 0 ) {
data = Serial.read();
dataReady = addData((char)data);
if ( dataReady )
processData();
}
}
bool addData(char nextChar)
{
static uint8_t currentIndex = 0;
if ((nextChar == '\n') || (nextChar == ' ') || (nextChar == '\t'))
return false;
if (nextChar == TERMINATOR_CHAR) {
g_buffer[currentIndex] = '\0';
currentIndex = 0;
return true;
}
g_buffer[currentIndex] = nextChar;
currentIndex++;
if (currentIndex >= MAX_DATA_LEN) {
g_buffer[MAX_DATA_LEN] = '\0';
currentIndex = 0;
return true;
}
return false;
}
void processData(void)
{
if ( strcmp(g_buffer, "ledon") == 0 ) {
digitalWrite(13, HIGH);
Serial.println("LED1 is on");
}
else if (strcmp(g_buffer, "ledoff") == 0 ) {
digitalWrite(13, LOW);
Serial.println("LED1 is off");
}
else if (strcmp(g_buffer, "ver") == 0 ) {
Serial.println("Version 1.0");}
else {
Serial.print("Unknown command ");
Serial.println(g_buffer);
}
}
Explanation of the Code
The processSerialData
function reads all available characters from the serial line and calls addData
() for each of them. The addData
function puts the new char
into buffer and decides if a complete command was received. For this decision, it simply checks whether the new char
is special terminator character TERMINATOR_CHAR
- in the code, I use the Enter key ('\r'
) but you can change it to something else.
The characters are stored into a C-style string
, that is an array of char
variables. One important thing to know about C strings is that the end of the string
is marked by a special character written as '\0
' and called NULL
character - which may be a confusing name but don't worry about it and just make sure you always put '\0
' at the end of your string
if you create it in a char
-by-char
way - just as the addData
function does. You don't need to add the '\0
' if you write the string
as char
s in double quotes, such as the "ledon"
string
in the processData
function. In such case, the compiler adds the terminating char
automatically. But always be sure to have space for this terminating char
in your array - see how the g_buffer
is declared with +1 to allow storing MAX_DATA_LEN
of usable char
s.
So the addData
function stores each character into the global string (array of char
s) g_buffer
at a position which is then increased, so that the next char
is stored as the next element in the array.
If complete command is received, the function processData
is called. This function is responsible for executing the commands. In the example, it just turns on/off the LED or prints the version of this program. To decide which command is received, the function must compare the string
stored in g_buffer
with supported commands. Note that you cannot compare string
s in C just like str1 == str2
. You would be comparing just the addresses of the string
s, but not the contents. You need to use standard library function strcmp
- string
compare. It returns 0
if the string
s are the same.
Trying the Code
To try it, just copy-paste the above code to your Arduino sketch, upload it to Arduino and open Serial monitor. First, select Carriage Return in the combo box in the bottom of the Serial monitor so that it sends the Return key when you click the Send button or press Enter on your keyboard. Type ledon
and press Enter to turn on LED on your Arduino board. If you enter invalid command, the program should tell you so.
Receiving Numbers from Serial Line
For those interested in passing some numbers to their programs, here is the modified version of the code which shows how to receive two numbers from the serial line. When sending numbers, you will need to define some protocol to be sure that the receiving program recognizes the numbers and does not mix them together - as explained above in the example with sending 001 002
, etc. In this program, I use human friendly notation but you can easily change it to suit your needs. So in my program, the input is expected in this format: "x=10y=20#
". There is x=
and y=
to mark the start of the numbers.
Here is the code. To try it, run it in your Arduino and from the Serial Monitor send, for example, x=10y=20#
. The program should respond with "New params received. x = 10, y = 20
".
#include <string.h> // for strcmp()
#define MAX_DATA_LEN (16)
#define TERMINATOR_CHAR ('#')
char g_buffer[MAX_DATA_LEN + 1];
void processSerialData(void);
void setup() {
Serial.begin(9600);
}
void loop() {
delay(100);
processSerialData();
}
void processSerialData(void) {
int data;
bool dataReady;
while ( Serial.available() > 0 ) {
data = Serial.read();
dataReady = addData((char)data);
if ( dataReady )
processData();
}
}
bool addData(char nextChar)
{
static uint8_t currentIndex = 0;
if ((nextChar == '\n') || (nextChar == ' ') || (nextChar == '\t'))
return false;
if (nextChar == TERMINATOR_CHAR) {
g_buffer[currentIndex] = '\0';
currentIndex = 0;
return true;
}
g_buffer[currentIndex] = nextChar;
currentIndex++;
if (currentIndex >= MAX_DATA_LEN) {
g_buffer[MAX_DATA_LEN] = '\0';
currentIndex = 0;
return true;
}
return false;
}
void processData(void)
{
char* n1pos = strstr(g_buffer, "x=");
int number1 = -1, number2 = -1;
if ( n1pos != NULL && strlen(n1pos) > 2) {
number1 = atoi(n1pos+2);
char* n2pos = strstr(g_buffer, "y=");
if ( n2pos != NULL && strlen(n2pos) > 2 )
number2 = atoi(n2pos+2);
}
if ( number1 >= 0 && number2 >= 0 ) {
Serial.print("New params received. x = ");
Serial.print(number1);
Serial.print(", y = ");
Serial.println(number2);
} else {
Serial.println("Wrong data format, please use x=123y=123");
}
}
Explanation of the Code
The core of the program is the same as in the first example. The difference is that the TERMINATOR_CHAR
is set to #
and the processData
function is now different. It tries to parse the input string
and obtain the two numbers from it. For this, it uses standard C function atoi
which converts string
to a number. If the numbers are obtained, they are printed to serial line. If not, an error message is printed. There is also strstr
function used to find the x=
and y=
substrings in the received string
.
The strstr
is a standard C function which searches for a substring in another string
. It returns NULL
if the string
is not found or the pointer to the beginning of the substring if it is found. I know that "pointers" are scary, but you don't need to understand all about them to use the strstr
. It may be helpful if I tell you just this: the name of an array in C is a pointer to its first element. A string
in C is an array of char
s. So the name of a string
is actually a pointer to the beginning of the string
and you can think about it as the string
. For example, g_buffer
is in fact a pointer to the received string
. If you define:
char* p = g_buffer;
the p
points to the received string
too.
In the code, I save the result of strstr
into char* n1pos
:
char* n1pos = strstr(g_buffer, "x=");
If the g_buffer
contains "aaax=12y=10#
" then n1pos
will point to the "x
" character. And since pointer is actually like a string
, you can thing about n1pos
as a string "x=12y=10#
" - the substring of g_buffer
starting at "x
".
And you can add numbers to pointers and thus move the beginning of the "string
" represented by n1pos
. I use n1pos+2
as the input for the atoi
function so that the function receives just the number starting after "x=
". It is as if I call it like atoi("12y=10#");
The extra char
s after the number 12 are not a problem; atoi
will stop the conversion when it encounters character which is not a number.
I hope I did not confuse things too much with this attempt to explain pointers in two paragraphs. Please take a look at some good tutorials on the internet if you want to learn more about them.
History
- 6th August, 2018: First version