I'm having trouble with what should be a simple bit of code to validate GPS sentences on a MEGA.
First some background.... GPS units generate a NMEA 'sentence' containing navigation data (position, speed, altitiude etc), the last two bytes (after the asterix) are checksum validation codes that can be used to confirm that no data has been corrupted.
This code has a very simple function, it reads the GPS sentences from port 1, calculates the checksum validation codes and compares them with the transmitted validation code and then sends the sentence to the screen. The problem is that about 10% of the records don't get validated correctly. I've worked out that the issue seems to be because the case statement that processes each GPS character as it's recieved is occasionally missing the "$" character that indicates the start of a new record.
Heres' the code (much of it cribbed from TinyGPS in case it looks familiar)....
char _nmea_checksum[2+1]; // NMEA sentence checksum (last two characters after the asterix)
char _nmea_rec[100+1]; // NMEA record (aka sentence)
byte _parity;
unsigned long _good_recs; // Number of records that pass checksum validation
unsigned long _bad_recs; // Number of records that fail checksum validation
bool _sentence_comp; // Set to true once the end of a a NMEA sentence is detected
byte _term_offset; // pointer indicating how far along a NMEA sentence the process is.
void setup()
{
Serial.begin(115200); // Baud rate for screen
Serial.println("##################################################################");
Serial.println("#################### START ###############################");
Serial.println("##################################################################");
Serial1.begin(57600); // Baud rate for GPS unit
}
void loop() {
if (Serial1.available())
{
Serial.print(Serial1.read(), BYTE);
}
//Check that there's data waiting to be read
while (Serial1.available())
{
//process current byte from the logger
if (processGpsChar(Serial1.read()))
{
//return true;
}
}
}
bool processGpsChar(char c)
{
// Firstly, check for possible corruption.
// NMEA data should (I think) be limited to 0-9, A-Z and the following punctiation .,$* (and of course a space, carriage return and line feed)
if ( (c>=48 && c<=57) || (c>=65 && c<=90) || (c>=42 && c<=46) || c==36 || c==32 || c==10 || c==13 )
{
// The character recieved from the GPS is VALID, display it
Serial.print(c);
}
else
{
// Invalid character
Serial.println("ERROR - Received unexpected ascii code <%d> from GPS");
Serial.print("ERROR - Received unexpected ascii code <");Serial.print(c);Serial.println("> from GPS");
}
// Read in NMEA sentences one character at a time.
switch(c)
{
case '\r':
//Linefeed - ignore this
break;
case '\n':
//Carriage return - marks the end of a record
//Check that the record is valid, if it is then
//process it.
// Check that the buffer hasn't overflowed
if ( strlen(_nmea_rec) > 100 )
{
Serial.print("ERROR - NMEA record too long");
}
byte checksum2;
// Convert the transmitted checksum from a two digit string to a decimal.
checksum2 = 16 * from_hex(_nmea_checksum[0]) + from_hex(_nmea_checksum[1]);
Serial.print("nmea_checksum=<");Serial.print(_nmea_checksum);
Serial.print(">, checksum2=<");Serial.print(checksum2,DEC);
Serial.print(">, parity=<");Serial.print(_parity,DEC);Serial.println(">");
//Check the calculated checksum (_parity) matches the transmitted checksum (checksum2).
if (checksum2 == _parity)
{
_good_recs++; // increment the good record counter
Serial.print("-Y-");
}
else
{
_bad_recs++; // increment the bad record counter
Serial.print("-N-");
}
// display the calculated checksum plus the good and bad record count
Serial.print(_nmea_checksum);Serial.print("-");Serial.print(_good_recs);Serial.print("-");Serial.println(_bad_recs);
break;
case '*':
// This marks the end of the NMEA sentence, and the begining of the
// validation characters.
if (_term_offset < sizeof(_nmea_rec))
{
_nmea_rec[_term_offset] = 0; //put a null terminator at the end of the string
}
_sentence_comp = true; // Flag the fact that the NMEA sentence is now complete
_term_offset = 0; // reset the pointer, it can now be used to populate the checksum
break;
case '$': // Start of record
Serial.println("nmea_start");
// Flush the stuff that needs flushing
nmea_start();
break;
default :
//any other character.
if (_sentence_comp == false)
{
//If the sentence is incomplete, add the character to the end of the current sentence
if (_term_offset < sizeof(_nmea_rec) - 1)
{
_nmea_rec[_term_offset] = c;
_nmea_rec[_term_offset+1] = 0;
_term_offset++;
}
// The parity check is a bitwise XOR of all characters received so far.
_parity = _parity ^ c; //bitwise exclusive OR
}
else
{
// The sentence IS complete, so these characters must be part of the checksum
if (_term_offset < 2)
{
// this character must be part of the checksum
_nmea_checksum[_term_offset++] = c;
}
}
break;
}
//return false - indicates a character returned, but the sentence is still incomplete
return false;
}
void nmea_start(void)
{
// flushes and resets stuff when a new record starts
_parity = 0; // reset the parity checker
_nmea_checksum[0] = 0; // Reset the checksum
_nmea_checksum[1] = 0;
_nmea_checksum[2] = 0;
_sentence_comp = false; // reset the flag for detecting the end of the sentence
_term_offset = 0; // reset the pointer
}
int from_hex(char a)
{
// Returns 0-9 for an input of '0'-'9' and 10-15 for an input of 'A'-'F'
if (a >= 'A' && a <= 'F')
return a - 'A' + 10;
else if (a >= 'a' && a <= 'f')
return a - 'a' + 10;
else
return a - '0';
}
And here are some examples of correctly processed records as they appear on the screen…
$nmea_start
GPGLL,,,,,184219.340,V,N*7A
nmea_checksum=<7A>, checksum2=<122>, parity=<122>
-Y-7A-1-1
$nmea_start
GPGSA,A,1,,,,,,,,,,,,,,,*1E
nmea_checksum=<1E>, checksum2=<30>, parity=<30>
-Y-1E-2-1
$nmea_start
GPGSV,1,1,00*79
nmea_checksum=<79>, checksum2=<121>, parity=<121>
-Y-79-3-1
Each NMEA record starts with the “$”, then the record itself gets displayed along with some variables.
Here’s and example of a record that has failed the checksum validation….
$GPGGA,184221.940,,,,,0,0,,,M,,M,,*49
nmea_checksum=<49>, checksum2=<73>, parity=<50>
-N-49-61-13
The big difference (apart from it not working!) is the absence of “nmea_start” between the “$” and the rest of the sentence. This seems to indicate an issue with the case statement in my code, if the start of the sentence isn’t recognized, then the various variables don’t get reset properly and the sentence fails validation .
My first guess was that there was interfacing issue between the GPS module and the Mega, but since the GPS data is being correctly displayed on the screen, then it must be getting read successfully, therefore the problem must be with my code.
Any suggestions?
The GPS unit is a Locosys LS20031
http://www.sparkfun.com/products/8975 connected to a Mega2560.