EM-406A GPS From Scratch - Part 2

Grade: Beginner

Having got data out of the EM-406A last time, we move on to interpreting what we get. Our aims today are:
  1. to read serial data from the GPS and assemble it into 'sentences'
  2. to validate the sentences we assemble and ensure they are exactly what the GPS sent out
I've aimed this at a 'beginner' grade with the expectation you'll be doing copy-and-paste coding. There are many programming concepts here that aren't covered.

Note:
I neglected to mention last time that my GPS shield from SparkFun as a switch on it marked DLINE UART. This is very helpful when programming and running. Switch to DLINE to allow the program to load without interference from the GPS device. When the program's loaded, switch to UART to connect the GPS to the Arduino.

Assembling Sentences

The manual for the EM-406A (page 2, Features) shows the this device can "Support NMEA 0183 data protocol" and indeed this is the default format of data coming out of the device. As we saw last time, data looks like this:

$GPRMC,202735.000,A,3246.4622,S,15136.1138,E,0.39,102.25,060412,,,A*77
$GPGGA,202736.000,3246.4622,S,15136.1136,E,1,08,0.9,20.8,M,26.6,M,,0000*7E
$GPGSA,A,3,05,19,08,17,28,13,11,26,,,,,1.9,0.9,1.7*3A
$GPRMC,202736.000,A,3246.4622,S,15136.1136,E,0.29,233.28,060412,,,A*77
$GPGGA,202737.000,3246.4622,S,15136.1132,E,1,08,0.9,20.7,M,26.6,M,,0000*74
$GPGSA,A,3,05,19,08,17,28,13,11,26,,,,,1.9,0.9,1.7*3A
$GPGSV,3,1,11,08,62,169,15,28,56,264,27,07,49,100,,26,28,222,26*72
$GPGSV,3,2,11,13,27,022,21,17,27,350,31,19,25,132,21,11,20,087,22*7A
$GPGSV,3,3,11,05,12,271,22,01,09,068,,10,08,323,*46
$GPRMC,202737.000,A,3246.4622,S,15136.1132,E,0.17,342.59,060412,,,A*7E

This looks nicely formatted to us but we can see from our code that we're only reading a single character at a time from the GPS and printing that to the Serial Monitor. The manual also shows (under the Software Command heading) that each NMEA output is followed by <cr><lf>. This is 'carriage return' and 'line fee' respectively, which the serial monitor interprets as a press of the <Enter> key, so each new sentence starts on a new line. What we want to do is capture all the characters until we reach the end of a line (sentence), then do something with the whole sentence.
We also need to verify the data has been read correctly from the GPS. It's for this reason each sentence has two check digits on the end - after the asterisk (*). Well, it turns out that we can kill two birds with one stone. We read the data one character at a time and append them to a sentence (a character array). If the character we're reading falls between the '$' at the start and the '*' at the end, we add that character to an exclusive-OR (X-OR) calculation. When we get to the end of the sentence, we compare the value we calculated (X-OR) with the last two characters of the sentence to see if we got it right. If it's right, we can use the sentence and also make assumptions about the formatting of the data. If it's wrong, we throw it away, there'll be another sentence just like it arriving in exactly one second.

Here's my new program:

static const byte GPS_BUFFER_SIZE = 100;
                  // The maximum size allowed for an NMEA sentence.
char sentence[GPS_BUFFER_SIZE];
                  // The character array where the NMEA sentence
                  // is stored.
byte sentenceIndex;
                  // The position in the sentence where the next
                  // character will be appended.
boolean checking; // Determines if characters arriving from GPS
                  // are X-OR'd for comparison to check digits.
char checkHex1, checkHex2;
                  // The last two characters of the NMEA sentence.
int checksum, checkDigit;
                  // checksum: the running total of X-OR operations
                  // on all the characters arriving from the GPS
                  // checkDigit: which of the check digits is
                  // is arriving next

//---------------------------------------------------------------------------------------------
void setup()
{
  Serial.begin(4800);
  Serial.println("Start!");
  sentenceIndex = 0;  // resetting all variables
  checking = false;
  checksum = 0;
  checkHex1 = 0;
  checkHex2 = 0;
  checkDigit = 0;
}

//---------------------------------------------------------------------------------------------
void loop()
{
  getGpsData();
  // This gets called repeatedly. As soon as it's finished
  // it's called again.
}

//---------------------------------------------------------------------------------------------
void getGpsData()
{
  while (Serial.available())
  // When there's no data available this function quits.
  {
    char character = Serial.read(); // get a single character
    switch (character) // decide based on the character received
    {
    case 10: // ASCII line feed 'LF'
      continue;  // just ignore it, restart at while()
    case 13: // ASCII carriage return 'CR'
      // This is the end of the sentence. Process the entire
      // sentence and restart for the next sentence.
      if (isValid()) // see function below
        Serial.print("Valid:   ");
      else
        Serial.print("INVALID: ");
      printSentence(sentence, sentenceIndex);
      Serial.println();
      sentenceIndex = 0;  // restart the sentence position
      checksum = 0;  // reset the checksum
      continue; // go back to while()
    case 42: // ASCII asterisk '*'
      // Stop adding characters to the sentence and instead
      // collect the check digit information.
      checking = false; // don't add remaining characters to 'checksum'
      checkDigit = 1; // next character will be first check digit
      continue;
    default:
      break;
    }
    if (checkDigit == 1)
    {
      checkHex1 = character;
      checkDigit++; // next character will be checkDigit 2
    }
    else if (checkDigit == 2)
    {
      checkHex2 = character;
      checkDigit = 0; // no more check digits
    }
    else sentence[sentenceIndex++] = character;
      // Append the current character to the sentence
    if (sentenceIndex == GPS_BUFFER_SIZE)
      // Check we haven't overrun the sentence array, if so, restart
    {
      sentenceIndex = 0;
      checksum = 0;
      continue;
    }
    if (checking) checksum ^= character;
      // X-OR character with previous X-OR sum
    if (character == 36) checking = true; // ASCII Dollar '$'
      // Leading $ is not included in checksum calculation, but
      // does indicate when to start calculating the checksum.
  }
}

//---------------------------------------------------------------------------------------------
boolean isValid()
// Compares the calculated [chechsum] to the check digits [checkHex1, checkHex2]
// supplied by the GPS. If the calculation matches the sentence is valid.
{
  return (charToHex(checkHex1) == (checksum >> 4) && charToHex(checkHex2) == (checksum & 0x0F));
}

//---------------------------------------------------------------------------------------------
byte charToHex(char in)
// Converts a hexadecimal character 
// {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}
// into its numeric equivalent
// { 0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,  15}
{
  if (in >= '0' && in <= '9') return in - '0';
  if (in >= 'A' && in <= 'F') return in - 'A' + 10;
  return 0;
}

//---------------------------------------------------------------------------------------------
void printSentence(char *sentence, byte length)
// Prints a specified number of characters from a character array
// and shows any non-printing characters by their ASCII number.
{
  for(int i = 0; i < length; i++)
  {
    if (sentence[i] < 32)
    {
      Serial.print('<');
      Serial.print(sentence[i], DEC);
      Serial.print('>');
    }
    else
    {
      Serial.print(sentence[i]);
    }
  }
}

Let's do a walk-through on the code. Everything above the line void setup() are variable declarations. This is the storage area for information the program uses. The setup() function (between the curly braces below  void setup()) starts the serial port which, as last time, is receiving data from the GPS and transmitting data to the Serial Monitor on the computer. The rest of setup() is ensuring all the variables declared above are initialised to a known value.

loop() is calling the function getGpsData(). That's all it does. When getGpsData() finishes, it gets called again.

All the code in getGpsData() is contained within a while loop that uses the Serial.available() condition. If there's no data available to read, the while loop ends, the getGpsData() function ends and control goes back to loop() where getGpsData() is called again and the cycle repeats. So, inside the while loop in getGpsData(), we read a single character from the GPS. We use a switch statement to perform different actions based on what kind of character we've received (look at the case statements below switch).

Now we need to stop and talk about the data we're receiving. The GPS is sending American Standard Code for Information Interchange (ASCII) characters. These are numbers that represent letters. As you can see from the ASCII table, the character 'A' has a numeric value of 65. We talked previously about the 'invisible' carriage return (CR) and line feed (LF) characters which have values 13 and 10 respectively.
Since we know ASCII characters have numeric values as well as character representations, we can use the two interchangeably. We can use characters or values and use the ASCII table to translate between them.


'A' == 65 // returns true.

So the first case we have in our switch statement is case 10: which is looking for a line feed character. If we see one of these we continue; return to the start of the while statement. Basically this character is ignored.

The next case is case 13:. Now this one is really interesting. This is the last character in every NMEA sentence, so when we see 13, we know we've got to the end of a sentence and the next character will be the start of another sentence. So now is the time to do anything we want to do with a whole sentence. We're calling the isValid() function to see if the sentence we've gathered up to this point is, well, valid! Because we're not doing anything interesting with the sentence yet, we're just printing it out with a prefix to say if it's valid or not. But this is where, later on, we'll pass the whole sentence along to another function that will pull it apart and extract the data it contains.

42 is the asterisk character. When we see that we switch from assembling the sentence to reading the check digits on the end. For any other character, we break out of the switch statement and carry on with the rest of the while loop.

    if (checkDigit == 1)
    {
      checkHex1 = character;
      checkDigit++; // next character will be checkDigit 2
    }
    else if (checkDigit == 2)
    {
      checkHex2 = character;
      checkDigit = 0; // no more check digits
    }
    else sentence[sentenceIndex++] = character;

The section above stores the check digits in their appropriate variables and, if not storing check digits, adds the character to the sentence. After that we check we haven't exceeded the buffer we allocated, because that would be a disaster.

    if (sentenceIndex == GPS_BUFFER_SIZE)

After that we recalculate the checksum including the character we just found:

    if (checking) checksum ^= character;

The last step is to restart checking (calculating the checksum) when we see the dollar sign. This ensures we don't include the dollar sign in the calculation as stated by the EM-406A's manual.

The isValid() function compares the calculated checksum to the two check digits provided by the GPS. If the calculation is correct, TRUE is returned, otherwise FALSE. There's an interesting aside here about binary numbers and why we use this odd way of calculating the correctness of the checksum, but I'm out of time for now. I'll point you to the operators available in C for more information.

The output of the program looks something like this. Each time you stop and start the GPS (by disconnecting it or switching the UART/DLINE swtich) you put a break in the data. This is detected and that sentence is marked INVALID. When whole sentences are received without error, they are marked Valid:. Note the last three characters are now missing from each sentence, we've taken care of the check digits and the asterisk, they've been used to validate the data and no longer needed.

Start!
INVALID: 12,,,,,,,3.3,1.8,2.8
Valid:   $GPRMC,233801.000,A,3246.4642,S,15136.1157,E,0.30,69.66,080412,,,A
Valid:   $GPGGA,233802.000,3246.4642,S,15136.1160,E,1,07,1.3,41.2,M,26.6,M,,0000
Valid:   $GPGSA,A,3,17,15,27,28,09,26,12,,,,,,2.6,1.3,2.3
Valid:   $GPRMC,233802.000,A,3246.4642,S,15136.1160,E,0.89,55.12,080412,,,A
Valid:   $GPGGA,233803.000,3246.4641,S,15136.1162,E,1,07,1.3,40.4,M,26.6,M,,0000
Valid:   $GPGSA,A,3,17,15,27,28,09,26,12,,,,,,2.6,1.3,2.3
Valid:   $GPGSV,3,1,11,27,55,234,27,17,53,145,26,04,47,026,,09,36,224,30
Valid:   $GPGSV,3,2,11,28,35,102,18,02,21,353,,15,20,278,30,26,19,322,23
Valid:   $GPGSV,3,3,11,08,15,034,,12,11,245,36,01,02,145,
Valid:   $GPRMC,233803.000,A,3246.4641,S,15136.1162,E,0.60,64.61,080412,,,A
Valid:   $GPGGA,233804.000,3246.4643,S,15136.1162,E,1,08,1.1,40.5,M,26.6,M,,0000

Feel free to send me feedback on this post.

That is all.

Comments

  1. I get $GPRMC,041114.230,V,,,,,,,080912,,,N
    need help im new

    ReplyDelete
  2. ""The next case is case 13:. Now this one is really interesting. This is the last character in every NMEA sentence, so when we see 13, we know we've got to the end of a sentence and the next character will be the start of another sentence""
    Dear i dont see 13 in the NMEA data ? every end of the sentence is different then the rest only $GPRMC ends with A.

    What is the use of sentence[sentenceIndex++] = character;

    ReplyDelete
  3. Hey Al-Yaqdhan Waleed. That's right, you won't see ASCII characters 10 and 13. They're "non-printing" characters (see http://www.asciitable.com/). But they do perform a useful function. When you use the basic code from Part 1 to print the NMEA data onto the Arduino Monitor, the characters 10 and 13 cause a "carriage-return and line-feed" to occur. This makes the next sentence appear on the next line. If they weren't there, all the data would be on one ridiculously huge line going left to right.

    sentence[sentenceIndex++] = character;
    // Append the current character to the sentence

    This line is a bit cryptic, sorry I should have explained that one better. Or, more correctly, I should have written better code.
    sentence[] is the array holding the NMEA sentence data.
    sentenceIndex is an integer holding the position the next character should be added onto sentence[].
    character is, obviously, the next single character that has arrived from the GPS.
    So in combination, the next single character "character" is added into the sentence array "sentence[]" at the position marked by the number "sentenceIndex". After sentenceIndex is used, it is incremented by the "++" that follows it. So the next time the line is run it points to the next position to put the character into.

    ReplyDelete

Post a Comment