Reading Numbers From Serial

Evan Boldt's picture

Introduction

Reading numbers from serial on an Arduino is needed surprisingly commonly. Exactly what is happening might be kind of hard to figure out.

Serial

Serial communication is digital, which means all data is transmitted in 1's and 0's. Typically, serial communication is done using ASCII letters. This means that, to send a number to the Arduino, the data sent is not the binary version of the number in base 2 (as an integer), but instead a sequence of characters for each digit in base 10 (which is human-readable). It is not an efficient use of bandwidth, but bandwidth is not usually a problem with Arduinos connect by USB.

Arduino Code

char commandLetter;  // the delineator / command chooser
char numStr[4];      // the number characters and null
long speed;          // the number stored as a long integer

void serialEvent() {
  // Parse the string input once enough characters are available
  if(Serial.available() >= 4) {
    commandLetter = Serial.read();
    
    //dump the buffer if the first character was not a letter
    if ( ! isalpha( commandLetter ) {
      // dump until a letter is found or nothing remains
      while(( ! isalpha( Serial.peak() ) && Serial.available()) {
        Serial.read(); // throw out the letter
      }
      
      //not enough letters left, quit
      if ( Serial.available() < 3 ) {
        return;
      }
    }
    
    // read the characters from the buffer into a character array
    for( int i = 0; i < 3; ++i ) {
      numStr[i] = Serial.read();
    }
    
    //terminate the string with a null prevents atol reading too far
    numStr[i] = '\0';
    
    //read character array until non-number, convert to long int
    speed = atol(numStr);
    Serial.println(speed);
  }
}

Code Explanation

serialEvent

serialEvent is a function that is called by Arduino before each loop. So, you can separate out your serial handling from the rest of your code. You could also put the contents of this function in the loop() function and it would be similar.

Fixed-Width

Commands need to be delineated in some way. Fixed-width commands are convenient on Arduino because the Serial.available function tells you how many characters are waiting. It is also possible to delineate by special characters. An extremely common and simple version of this is a Comma Separated Value file. However, since it is simplest to use C strings, the character arrays which the serial is read into is necessarily fixed-width anyway. To get around this issue, a vector or linked list could be used, so the serial string can dynamically grow and shrink.

Command Differentiation

In many cases, a robot takes multiple kinds of inputs. For example, there might be multiple speeds, or a direction, or some kind of trigger. Prefixing each command with a unique letter can make it easy to differentiate what the following string is supposed to do.

C Strings

C strings are arrays of char data types. They must be terminated by a NULL character '\textbackslash 0'. Without the NULL character at the end, there is no way for any function to know how long the string is. For example, a print function would print the string and then a bunch of garbage characters until it gets to a NULL or the end of the memory available. In this case, it's less likely for that to happen since we are only using atol, which will only read until there is a non-numeric character. A non-numeric byte is more likely to randomly be at the end of the string than a NULL character.

ASCII Table
Decimal Character
48 '0'
49 '1'
50 '2'
51 '3'
52 '4'
53 '5'
54 '6'
55 '7'
56 '8'
57 '9'

Char to Integer

Converting a single character to a number takes advantage of how characters are stored, which is typically in a format called ASCII. Think of a char of ASCII like an integer data type. Except that when printed, it is not a decimal number. The number in the int is translated into a letter on the screen according to a table. For example, the decimal number 97 is translated into the letter 'a'. Each letter is really stored as a 'number'. I'm showing it as a decimal (base 10) number, but it is really stored in binary (base 2). The C++ compile takes care of translating our base 10 decimal numbers into base 2 binary.

Here is how to take advantage of this:

int number = (int)charInput - (int)48;
// or:
int number = charInput - '0';

Since ASCII stores the letters sequentially, starting at 0, just subtract the value where 0 begins from each of the characters to convert it to its numeric form. This works regardless of the base of the character storage. So, it converts the ASCII character representing the number in base 10 into an integer in base 2.

Char Array (C-string) to Long Integer

Now, each of the letters from right to left need to be multiplied by a power of 10. Here is the basic idea of what atol does:

char charArray[5] = "5432"; // double quotes adds the \0 at the end

long longnum = 0;  //the converted number
long exp = 1;      //start with 10^0
int index = 0;     //the current place in the array

//find the end of the string - when character is not a number
while ( isdigit( charArray[idx] ) ) {
    ++idx;
}

//begin base multiplication and number conversion
while( idx >= 0 ) {
    //convert digit character to a long
    long currentDigit = (long) charArray[idx] - (long)48
    
    currentDigit *= exponent; //number multiplied by 10^(len-idx)
    exponent *= 10; // next number to left is another power of ten
    
    longnum += currentDigit;
--idx; }