NeoGPS - configurable, ultra-small RAM footprint

After wrestling with the terrible NMEA protocol for the nth time in my career, and realizing how many Arduino libraries are so wasteful of RAM, I decided to write a fully configurable GPS library, NeoGPS. It is positively stingy with RAM. Tight-fisted, even. :slight_smile:

  • The minimal NeoGPS configuration requires only 10 bytes
  • The full NeoGPS configuration requires only 43 bytes
    The best implementation I've found so far is TinyGPS, and it has been a great inspiration. It uses 120 bytes per instance plus another 60 bytes of character data. A similarly-configured NeoGPS requires 73 bytes.

TinyGPS is also a great example of how to support different program structures. Most people will use it in a polled fashion: check for serial byte, then process serial byte. But it also supports serial interrupt processing, even though the Arduino framework does not. (google for "Arduino Serial Interrupt" to get an idea of that general approach... it requires modifying HardwareSerial.cpp)

Most libraries have extra buffers so that parts of the sentence can be parsed all at once. For example, an extra field buffer may hold on to all the characters between commas. That buffer is then parsed into a single data item, like heading. Some libraries even hold on to the entire sentence before attempting to parse it. This also requires extra CPU time to copy the bytes and index through them... again.

  • NeoGPS parses each character immediately into the data item. When the delimiting comma is received, the data item has been fully computed and is marked as valid.Most libraries parse all fields of their selected sentences. Although most people use GPS for obtaining lat/long, some need only time, or even just one pulse-per-second.

  • NeoGPS configures each item separately. Disabled items are conditionally compiled, which means they will not use any RAM, program space or CPU time.Most libraries treat sentences as independent from each other. A long-running frustration has been that multiple GPS sentences must be received in order to know all the information about the current fix: position, velocity, accuracy, etc.

  • NeoGPS can group information from multiple sentences into one fix.If you need a coherent fix, you must also correlate the sentences by time. By coherent, I mean that the Position at a particular time is grouped with the Velocity at that same time. BTW, Some sentences have time, some do not. Who thought this up, anyway? :angry:

  • NeoGPS can correlate multiple sentences into one coherent fix.Most libraries require float-point support. This can be caused by their data types, the use of scanf, or even distance/heading calculations.

  • NeoGPS uses integer representations for all members, preserving their full accuracy.Optional accessors can convert the members to floating-point, if that's what you really need.

Furthermore, GPS device manufacturers have recognized the inherent deficiencies in the NMEA protocol, and frequently provide their own proprietary NMEA sentences or even their own protocol.

  • Classes can be derived from NeoGPS to implement additional protocols or NMEA sentences.My particular device is the u-blox NEO-6M, so I have provided a derived class ubloxNMEA for its proprietary NMEA sentences. For most devices, it should be as simple as identifying the sentences in a table and parsing the specific types in parseField

I have also provided a second derived class ubloxGPS for the UBX protocol. This class shows how to chain multiple protocols from the same device. It also shows how to accumulate a fix as it is received, without any buffering.

Tradeoffs

There's a price for everything, hehe...

  • Parsing without buffers, or in place, means that you must be more careful about when you access data items.In general, you should wait to access the fix until after the entire sentence has been parsed (See loop() in NMEA.ino). Member function coherent() can also be used to determine when it is safe. If you need to access the fix at any time, you will have to double-buffer the fix (See NMEAGPS.h comments regarding a safe_fix). Also, data errors can cause invalid field values to be set before the CRC is fully computed. The CRC will catch most of those, and the fix members will be marked as invalid at that time.

  • Configurability means that the code is littered with #ifdef sections.I've tried to increase white space and organization to make it more readable, but let's be honest... conditional compilation is ugly.

  • Accumulating parts of a fix into group means knowing which parts are valid.Before accessing a part, you must check its valid flag. Fortunately, this adds only one bit per member. See GPSfix.cpp for an example of accessing every data member.

  • Correlating timestamps for coherency means extra date/time comparisons for each sentence before it is fused.See NMEAfused.ino for code that determines when a new time interval has been entered.

  • Full C++ OO implementation is more advanced than most Arduino libraries, but it's is a good way to support future capabilities.You've been warned! :wink:

  • "fast, good, cheap... pick two."Although most of the RAM reduction is due to eliminating buffers, some of it is from trading RAM for additional code. And, as I mentioned, the readabilty (i.e., goodness) suffers from its configurability.

Well, if you're super constrained by RAM or need better performance or fix fusion, please take a look.

Cheers,
/dev

P.S. There are a few things I will be adding shortly: GPGST, GPGSA, and GPGSV sentences, along with their field types, and maybe some of the most popular proprietary sentences (with derived classes).

P.P.S. I am currently using 1.0.5

The GPGST, GPGSA and GPGSV sentences are now implemented. They provide VDOP, PDOP, lat/lon/alt errors in centimeters, and information about the satellite constellation.

I also gathered some performance and PROGMEM stats. As I suspected, the NeoGPS CPU time per sentence is about two-thirds that of TinyGPS: about 1000uS vs 1400uS.

For the same configuration, NeoGPS takes a little more, 2800 bytes of program space, while TinyGPS takes 2400 bytes. NeoGPS can be configured to be as small as 866 bytes or as large as 3492 bytes. Please see the README for a complete breakdown.

Cheers,
/dev

Does the library require a UART? On an Uno, I have to use some form of software serial to communicate with a GPS.

No, it just requires a byte. :slight_smile: I think I see two left-overs from my debugging that imply the need for UART or Serial... not necessary. I'll go nix those.

All the example programs use Serial for debug output and Serial1 for reading GPS bytes. Those bytes are then given to the library. Internally, NeoGPS doesn't do anything with a UART -- it just uses the bytes passed in.

In your case, you'll do a SoftwareSerial read to get a byte. Then pass that byte to gps.decode just like in the examples. Something like this:

SoftwareSerial ss( rxpin, txpin );
...
void loop()
{
  while (ss.available())
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      //  Got a sentence, do something with gps.fix()...

If you need to send something to the GPS device, like a configuration or reset command, just pass your SoftwareSerial instance to send:

   // ask for a PUBX sentence
   gps.send_P( &ss, F("PUBX,00") );

Cheers,
/dev

Glad to hear that. There's a code snippet in the readme file that refers to "uart1.available()" which was why I wondered.

I tried all of your examples with "NMEA" as part of the filename. None of these produced any output of interest to me, just a few lines about an object size. I was hoping for a demo that would print out parsed GPS values, like the TinyGPS example does.

I wanted to give it a spin and compare it to what I'm currently doing. But I'm not sure how to use this library.

> None of these produced any output of interest to me, just a few lines about an object size.

Hmmm, all you're seeing is the startup banner. Like you expected, it should print out the parsed GPS values after a GPRMC sentence is received. Could you give me a little information about your setup? I see you're using an Uno, but what GPS device do you have?

The default configuration is just like TinyGPS: it only parses GPGGA and GPRMC. If you simply reload with TinyGPS and it works, your device must put out one or both of those. I suspect there's something different about your Software Serial. Maybe check the 9600 baud rate. Could you post your version of NMEA.ino here so I could give it a try?

Here's one thing to test: in loop(), move the sentence_received() call above the if statement:

      sentenceReceived(); // moved
      if (gps.nmeaMessage == NMEAGPS::NMEA_RMC) {

        //  Use received GPRMC sentence as a pulse
        seconds++;
      }

If you start getting output, that would mean your device is not emitting a GPRMC. That would be unusual, so I really suspect the SoftwareSerial.

Thanks,
/dev

I have a Global Top PA6H GPS module.

The Arduino SoftwareSerial doesn't work very well so I wrote my own version. What specific requirements does your GPS code have in the way of a serial library? I've been using this software serial at 38400 baud in a GPS logging sketch without issue.

For testing with your library I programmed the GPS to output GGA and RMC once per second at 9600 baud. I tried moving the sentence_received() call but it had no effect.
Here is NMEA.ino:

#include <zSoftSerial.h>
zSoftSerial Serial1;

/*
  Serial is for trace output.
  Serial1 should be connected to the GPS device.
*/

#include <Arduino.h>

#include "NMEAGPS.h"
#include "Streamers.h"

// Set this to your debug output device.
Stream & trace = Serial;

static uint32_t seconds = 0L;

static NMEAGPS gps;

//--------------------------

static void sentenceReceived()
{
#if !defined(GPS_FIX_TIME) & !defined(GPS_FIX_DATE)
  //  Date/Time not enabled, just output the interval number
  trace << seconds << ',';
#endif

  trace << gps.fix();

#if defined(NMEAGPS_PARSE_SATELLITES)
  if (gps.fix().valid.satellites) {
    trace << ',' << '[';

    uint8_t i_max = gps.fix().satellites;
    if (i_max > NMEAGPS::MAX_SATELLITES)
      i_max = NMEAGPS::MAX_SATELLITES;

    for (uint8_t i=0; i < i_max; i++) {
      trace << gps.satellites[i].id;
#if defined(NMEAGPS_PARSE_SATELLITE_INFO)
      trace << ' ' << 
        gps.satellites[i].elevation << '/' << gps.satellites[i].azimuth;
      trace << '@';
      if (gps.satellites[i].tracked)
        trace << gps.satellites[i].snr;
      else
        trace << '-';
#endif
      trace << ',';
    }
    trace << ']';
  }
#endif

  trace << '\n';

} // sentenceReceived

//--------------------------

void setup()
{
  // Start the normal trace output
  Serial.begin(9600);
  trace.print( F("NMEA test: started\n") );
  trace.print( F("fix object size = ") );
  trace.println( sizeof(gps.fix()) );
  trace.print( F("NMEAGPS object size = ") );
  trace.println( sizeof(NMEAGPS) );
  trace.flush();
  
  // Start the UART for the GPS device
  Serial1.begin(9600);
}

//--------------------------

void loop()
{
  while (Serial1.available())
    if (gps.decode( Serial1.read() ) == NMEAGPS::DECODE_COMPLETED) {
//      trace << (uint8_t) gps.nmeaMessage << ' ';

// Make sure that the only sentence we care about is enabled
#ifndef NMEAGPS_PARSE_RMC
#error NMEAGPS_PARSE_RMC must be defined in NMEAGPS.h!
#endif

        sentenceReceived();    // moved from below
      if (gps.nmeaMessage == NMEAGPS::NMEA_RMC) {
//        sentenceReceived();

        //  Use received GPRMC sentence as a pulse
        seconds++;
      }
    }
}

Here is a simple sketch that verified that the GPS was outputting correctly (it was):

#include <zSoftSerial.h>
zSoftSerial Serial1;

void setup() {
  Serial.begin(9600);
  Serial1.begin(9600);
}

void loop() {
  while (Serial1.available())
    Serial.write(Serial1.read());
}

Here is a sketch with TinyGPS which works well:

#include <zSoftSerial.h>
zSoftSerial ss;

#include <TinyGPS.h>
TinyGPS gps;

static void smartdelay(unsigned long ms);
static void print_float(float val, float invalid, int len, int prec);
static void print_int(unsigned long val, unsigned long invalid, int len);
static void print_date(TinyGPS &gps);
static void print_str(const char *str, int len);


void setup() {
  Serial.begin(9600);
  ss.begin(9600);
  Serial.print("Testing TinyGPS library v. "); Serial.println(TinyGPS::library_version());
  Serial.println("by Mikal Hart");
  Serial.println();
  Serial.println("Sats HDOP Latitude  Longitude  Fix  Date       Time     Date Alt    Course Speed Card  Distance Course Card  Chars Sentences Checksum");
  Serial.println("          (deg)     (deg)      Age                      Age  (m)    --- from GPS ----  ---- to London  ----  RX    RX        Fail");
  Serial.println("-------------------------------------------------------------------------------------------------------------------------------------");


}

void loop()
{
  float flat, flon;
  unsigned long age, date, time, chars = 0;
  unsigned short sentences = 0, failed = 0;
  static const double LONDON_LAT = 51.508131, LONDON_LON = -0.128002;
  
  print_int(gps.satellites(), TinyGPS::GPS_INVALID_SATELLITES, 5);
  print_int(gps.hdop(), TinyGPS::GPS_INVALID_HDOP, 5);
  gps.f_get_position(&flat, &flon, &age);
  print_float(flat, TinyGPS::GPS_INVALID_F_ANGLE, 10, 6);
  print_float(flon, TinyGPS::GPS_INVALID_F_ANGLE, 11, 6);
  print_int(age, TinyGPS::GPS_INVALID_AGE, 5);
  print_date(gps);
  print_float(gps.f_altitude(), TinyGPS::GPS_INVALID_F_ALTITUDE, 7, 2);
  print_float(gps.f_course(), TinyGPS::GPS_INVALID_F_ANGLE, 7, 2);
  print_float(gps.f_speed_kmph(), TinyGPS::GPS_INVALID_F_SPEED, 6, 2);
  print_str(gps.f_course() == TinyGPS::GPS_INVALID_F_ANGLE ? "*** " : TinyGPS::cardinal(gps.f_course()), 6);
  print_int(flat == TinyGPS::GPS_INVALID_F_ANGLE ? 0xFFFFFFFF : (unsigned long)TinyGPS::distance_between(flat, flon, LONDON_LAT, LONDON_LON) / 1000, 0xFFFFFFFF, 9);
  print_float(flat == TinyGPS::GPS_INVALID_F_ANGLE ? TinyGPS::GPS_INVALID_F_ANGLE : TinyGPS::course_to(flat, flon, LONDON_LAT, LONDON_LON), TinyGPS::GPS_INVALID_F_ANGLE, 7, 2);
  print_str(flat == TinyGPS::GPS_INVALID_F_ANGLE ? "*** " : TinyGPS::cardinal(TinyGPS::course_to(flat, flon, LONDON_LAT, LONDON_LON)), 6);

  gps.stats(&chars, &sentences, &failed);
  print_int(chars, 0xFFFFFFFF, 6);
  print_int(sentences, 0xFFFFFFFF, 10);
  print_int(failed, 0xFFFFFFFF, 9);
  Serial.println();
  
  smartdelay(1000);
}

static void smartdelay(unsigned long ms)
{
  unsigned long start = millis();
  do 
  {
    while (ss.available())
      gps.encode(ss.read());
  } while (millis() - start < ms);
}

static void print_float(float val, float invalid, int len, int prec)
{
  if (val == invalid)
  {
    while (len-- > 1)
      Serial.print('*');
    Serial.print(' ');
  }
  else
  {
    Serial.print(val, prec);
    int vi = abs((int)val);
    int flen = prec + (val < 0.0 ? 2 : 1); // . and -
    flen += vi >= 1000 ? 4 : vi >= 100 ? 3 : vi >= 10 ? 2 : 1;
    for (int i=flen; i<len; ++i)
      Serial.print(' ');
  }
  smartdelay(0);
}

static void print_int(unsigned long val, unsigned long invalid, int len)
{
  char sz[32];
  if (val == invalid)
    strcpy(sz, "*******");
  else
    sprintf(sz, "%ld", val);
  sz[len] = 0;
  for (int i=strlen(sz); i<len; ++i)
    sz[i] = ' ';
  if (len > 0) 
    sz[len-1] = ' ';
  Serial.print(sz);
  smartdelay(0);
}

static void print_date(TinyGPS &gps)
{
  int year;
  byte month, day, hour, minute, second, hundredths;
  unsigned long age;
  gps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &hundredths, &age);
  if (age == TinyGPS::GPS_INVALID_AGE)
    Serial.print("********** ******** ");
  else
  {
    char sz[32];
    sprintf(sz, "%02d/%02d/%02d %02d:%02d:%02d ",
        month, day, year, hour, minute, second);
    Serial.print(sz);
  }
  print_int(age, TinyGPS::GPS_INVALID_AGE, 5);
  smartdelay(0);
}

static void print_str(const char *str, int len)
{
  int slen = strlen(str);
  for (int i=0; i<len; ++i)
    Serial.print(i<slen ? str[i] : ' ');
  smartdelay(0);
}

> The Arduino SoftwareSerial doesn't work very well so I wrote my own version.

Awesome. You're smarter than the average bear! :smiley:

> What specific requirements does your GPS code have in the way of a serial library?

None, really. Just "gimme a byte whenever."

Since you confirmed that the device is sending those two sentences, I suspect they are being rejected for some reason. CRC comes to mind: I've encountered what I thought was an invalid example sentence in the manufacturer document. Or perhaps NeoGPS is rejecting a field value. I also noticed that TinyGPS does not reject characters outside the range ' '..'~', while NeoGPS does.

> I have a Global Top PA6H GPS module.

I just tried some example sentences from their manual, and they are being rejected. :-[ We are experiencing technical difficulties, please stand by...

Could you paste some of the output from your simple echo program? Just to double check...

Thanks,
/dev

Ok, it wasn't real obvious, but I found this in their manual:

| Name | | Example | | Units | | Description |
| - | - | - | - |
| UTC Time | | 064951.000 | | | | hhmmss.sss |

Looks fine, yes? Grrr... there is one extra digit of precision: .SSS. They track to milliseconds. While TinyGPS discards the digit, I reject it as an invalid value.

I'll check in a version that does the same and review some of the other rejections. Then I think I'll increase the precision of NeoGPS time to milliseconds. I always disliked centiseconds, anyway. :wink:

Cheers,
/dev

I only bother with tenths of a second. This GPS maxes out at 10 Hz and with the lag between fix and transmission and then decode I don't think a precision of 10ms is very useful, never mind 1ms.

I haven't spent that much time looking into it but I know of at least one other GPS that reports the time to a precision of milliseconds (u-blox). I seem to recall running across one that output a greater precision of lat/lon data as well. I got the feeling that the NMEA strings are a loose standard and any decoding software would either have to know which device it was working with or be very flexible and forgiving.

Fixt. There were 3 or 4 other fields that were a little restrictive, so I relaxed those as well. Please start with NMEA, NMEAfused or NMEAtest. I've got some merging to do on the others.

> a precision of milliseconds (u-blox)

Yes, that's the unit I have, but ms only appear in the UBX protocol time-of-week. The format of their NMEA time field is declared to be .SSS, but the examples (and the device) use .SS

> I got the feeling that the NMEA strings are a loose standard...

...using the term loosely.

> I don't think a precision of 10ms is very useful, never mind 1ms.

Certainly not for anything I would do. I am thinking about tossing this over to the ArduPilot boys. I think I saw that they parse mS into 5Hz buckets, so even they don't care about 100Hz.

However, they might appreciate the extra 500uS/sentence or so that you can get with my techniques. Their code appears to be based on TinyGPS. Maybe after a few more people have tried it.

Cheers,
/dev

I can believe that somebody wants that precision; just not me. Actually, the module I have has a very precise (±10ns) 1 Hz pulse output that is coincident with the GPS time. If someone were using that signal a 1ms precision might be wanted.

I downloaded your latest code and it is indeed spitting out what appears to be the fields from the RMC.

But I have to confess that I still don't know how to use your library. My C++ skills are very poor and I'm used to using crude techniques like Serial.println(something).

Suppose I want to print out the GPS speed from the RMC and the altitude from the GGA string. What do I have to do?

> But I have to confess that I still don't know how to use
> your library. My C++ skills are very poor and...

Yes, it's definitely your fault! :slight_smile: Actually, that's very gracious of you. Writing libraries for reuse is very hard, and I'm obviously deficient in documenting it. The first "reuser" inevitably suffers through not being able to read the developer's mind. Let's see...

I think the key concept is a fix. A fix is the collective term for everything that's known about the physical state. By "physical", I really mean the physics terms: position, velocity and acceleration.

GPS devices figure out position in terms of Latitude, Longitude and Altitude (or Earth-Centered x, y and z); and velocity in terms of Speed and Heading (or Velocity North, East and down). Some GPS devices have extra sensors (or interfaces to extra sensors) that provide acceleration. Of course many projects can 'fuse' information from an IMU, barometer, compass, etc. to calculate these into one fix.

Unfortunately, the NMEA sentences don't provide a fix in one sentence. We programmers have the delightful task of parsing multiple sentences in order to accumulate all the parts of a fix. You have already learned that Speed is in the RMC and the Altitude is in the GGA:

> How do I print out the GPS speed from the RMC and the altitude from the GGA?

To answer your specific question: NeoGPS parses each sentence into gps.fix(), and a speed field gets parsed into gps.fix().speed. The gps.fix().valid.speed flag is also set to true so you know which members have been filled out. (See GPSfix.cpp for an example of accessing each member.) So you should write something like this:

 while (ss.available())
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      if (gps.nmeaMessage == NMEAGPS::NMEA_RMC) {
        if (gps.fix().valid.altitude)
          Serial.println( gps.fix().altitude_cm() / 100 ); // meters
      } else if (gps.nmeaMessage == NMEAGPS::NMEA_GGA) {
        if (gps.fix().valid.speed)
          Serial.println( gps.fix().speed_mkn() / 1000 ) // knots
      }
    }
  }

I structured NeoGPS so that you could get just these two pieces, if that's all you want. No extra copies, no extra buffers, no time wasted parsing lat/lon, etc. All other libraries have those inefficiencies.

Truly, if you use those values immediately, that is all you should have to do.

  • Warning: if you remember the Tradeoffs section in the README, you'll know that you can't access gps.fix() at any other time than after a DECODE_COMPLETED. Those values could be only half-parsed from the incoming bytes; they would be nonsense.However... (TL;DR on its way)

...many programs need to use multiple parts at the same time (or at any time).

Let's say you didn't want to print one line for alt and a separate line for speed. You'd probably like them on one line. So you have to save them one at a time, and then print them together at a later time:

  uint32_t alt;
  uint32_t speed;

  while (ss.available()) {
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      if (gps.nmeaMessage == NMEAGPS::NMEA_RMC) {
        if (gps.fix().valid.altitude)
          alt = gps.fix().altitude_cm() / 100; // meters
      } else if (gps.nmeaMessage == NMEAGPS::NMEA_GGA) {
        if (gps.fix().valid.speed)
          speed = gps.fix().speed_mkn() / 1000; // knots
  } } }

  if (time_to_print())
    Serial.print( speed );
    Serial.print( F(", ") )
    Serial.println( alt );
  }

Ok, not too bad. We have saved two parts, and they can be used at any time.

What if you want to be sure that the speed was measured at the same time as the altitude? Then you've got to look at the time stamps. Date and Time are parsed into gps.fix().dateTime.

  uint32_t  alt;
  uint32_t  speed;
  dateTime  last_dt;
  uint8_t   parts = 0;

  while (ss.available()) {
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      if (gps.fix().valid.time) {
        if (last_dt != gps.fix().dateTime) {
          //  We're in the next time interval, print out
          //     the parts from the last interval if I got them.
          if (parts == 2) {
            Serial.print( speed );
            Serial.print( F(", ") )
            Serial.println( alt );
          }
          parts = 0;
          last_dt = gps.fix().dateTime;
        }
      }

      if (gps.nmeaMessage == NMEAGPS::NMEA_RMC) {
        alt = gps.fix().altitude_cm() / 100; // meters
        parts++;
      } else if (gps.nmeaMessage == NMEAGPS::NMEA_GGA) {
        speed = gps.fix().speed_mkn() / 1000; // knots
        parts++;
      }
    }
  }

Urk. Add in the GPS status (tracking or not) and the code just gets uglier.

I think we can avoid all this mess.

First, let's ignore the whole sentence type:

  uint32_t alt;
  uint32_t speed;

  while (ss.available()) {
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      if (gps.fix().valid.speed)
        speed = gps.fix().speed_mkn() / 1000;
      if (gps.fix().valid.altitude)
        alt = gps.fix().altitude_cm() / 100;

If the speed was received (in any message), we'll save it for later. In general, check gps.fix().valid.XXX before you try to get the value for gps.fix().XXX().

Notice that we had to declare extra variables to hold the speed and altitude. You'll need an extra copy of all the fix members that you care about. Well, why don't we just use an instance of gps_fix?

gps_fix myFix;

void loop()
{
  while (ss.available()) {
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      if (gps.fix().valid.speed) {
        myFix.speed = gps.fix().speed;
        myFix.valid.speed = true;
      }
      if (gps.fix().valid.altitude) {
        myFix.altitude = gps.fix().altitude;
        myFix.valid.altitude = true;
      }

Wait, there are a whole bunch of fix members. How about a subroutine to copy them all at once? Just the valid ones, that is. Yes, there is a method to do that. It's a C++ operator, though, so the name is a little weird, and it's invoked like a C operator:

gps_fix myFix;

void loop()
{
  while (ss.available())
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED)
      myFix |= gps.fix();  // <-- calls the copy routine "operator |="

  if (time_to_print()) {
    if (myFix.valid.speed)
      Serial.print( myFix.speed_mkn() / 1000 );
    Serial.print( F(", ") );
    if (myFix.valid.altitude)
      Serial.println( myFix.altitude.whole );
  }

Mmmmm, much nicer! This is approximately how most libraries work, and this is how NMEAfused.ino works. They have an extra copy for use at any time. When a sentence is parsed sucessfully, the new values are copied.

BTW, I have not seen any libraries that provide coherent fixes. Please see NMEAcoherent.ino for the that technique.

More than you wanted to know? :slight_smile: Well, I hope that helps, or leads to another question.

Cheers,
/dev

P.S. I think I need a Configuration section in the README to describe how to enable and disable the messages and parts of a fix. Real Soon Now.

/dev:
More than you wanted to know? :slight_smile: Well, I hope that helps, or leads to another question.

Less than what I wanted to know, actually. I can understand why documentation is frequently left for last, or left off entirely.

But I think I know enough now to compare your library to what I'm currently doing.

> Less than what I wanted to know, actually.

You're a glutton for punishment. Maybe these new README sections would be of interest: Configuration and Extending NeoGPS. The latter describes three ways to add device-specific behavior.

Cheers,
/dev

Thanks for the extra information. I still had to look at the code to figure out how to print out all of the fields I was interested in, but it wasn't too big of a deal.

I wrote a simple sketch to print out all of the values from the GGA and RMC and it fell together without much effort.

One thing I noticed is that dateTime.Year is returning 45 instead of 15 or 2015.

I like what you've done.

I wanted to do some sort of comparison between NeoGPS and the code I'm using. It's difficult to do a fair apples to apples comparison since it depends on the details of the test. Also, since I am writing code for a specific device and a specific application I can tailor it to the exact requirements.

That said, I took a simple sketch which acquired GGA and RMC strings once per second at 38400 baud and then printed out all of the values.

NeoGPS: object size = 43
my GPS: object size = 86

NeoGPS: sketch = 9950; SRAM = 515
my GPS: sketch = 6532; SRAM = 457

Then I compared parsing time, using the GGA and RMC strings in your benchmark example

NeoGPS: GGA = 1008 us; RMC = 1060 us
my GPS: GGA = 448 us; RMC = 468 us

I was mostly surprised that my code used less ram since it stores most of the data as characters instead of encoding it as integers or floats. But the software serial has a 64 byte buffer, something I was able to do away with in my code by calling the parse function from the ISR with interrupts re-enabled. It might be possible to do the same thing using NeoGPS but it would require some modification to the library.

Apples to apples? Probably not. But at first glance it doesn't look like NeoGPS will save me resources on this particular project. I'm a little disappointed since your library is so much more sophisticated than mine.

NeoTest_06Jan.ino (2 KB)

uGpsTest2_06Jan.ino (1.64 KB)

Interesting work!

> One thing I noticed is that dateTime.Year is returning 45 instead of 15 or 2015.

Yes, that Time library has an epoch of 1970. The Year is really an offset. I prefer an epoch of 2000, or even , as date-to-seconds conversion is much quicker. Thanks, Mr. February!

> NeoGPS: object size = 43
> my GPS: object size = 86

I would suggest disabling the virtuals. It's not a big effect, though.

> NeoGPS: sketch = 9950; SRAM = 515
> my GPS: sketch = 6532; SRAM = 457

That's a number alright. Is that a high-water mark? Well, I can tell you that NeoGPS does not use any more RAM than its object size... except when you call it, and then it's using the stack. When it returns from the call, that RAM is "returned". There are many sneaky ways that RAM can get used, so I'd have to look closer to explain what the numbers mean, and whether they are both apples.

Here's something that caught my eye:

  Serial.print("Memory used: "); Serial.println(2048 - freeMemory());

This is executed after a sentence is decoded, so I'm not sure what it's measuring. And it turns out that string literals use RAM. For a pleasant surprise, I would suggest replacing all the string literals with F("string literal"), at least the ones that are printed. Boy, this is a topic in itself.

> Then I compared parsing time...
>
> NeoGPS: GGA = 1008 us; RMC = 1060 us
> my GPS: GGA = 448 us; RMC = 468 us

I think the times actually makes sense, given that uGPS...

> ... stores most of the data as characters instead of endecoding it
> as integers or floats.

Most libraries do not do math very well. And just to clarify, NeoGPS does not do any floating-point math while parsing. You can access the integers as if they were floats, because it does a conversion when you ask for it, long after the parsing is done. These are the "floating-point accessors" referred to in the README. NeoGPS is very careful about division, modulo (mostly a divide), and 32-bit math operations because they are much slower on an 8-bit processor. Fortunately, it does have 8- and 16-bit multiply instructions that execute in one cycle.

I originally considered not decoding the integers: the bytes could stored as BCD. It's fairly dense (two digits per byte) and fairly easy to decode. The RAM size, however, was still larger than I wanted. After looking at the assembly, I realized it didn't take any more time to do a multiply-by-ten than doing a left-shift 4 for BCD.

Back to the timings. I think they're probably right, given the operations needed to convert characters to ints. Don't even get me started on the funky DDDmm.mmm format! Sheesh, 1-3 digits of degrees, no separator, then floating-point minutes?!?

It really gets down to whether you need the information from the device, or just the bytes. If you are comparing values or calculating a distance, you need the numbers. This is the NeoGPS et al niche.

If you are just logging or displaying the bytes, perhaps in a different order, you only need... bytes. A much simpler problem.

> ...by calling the parse function from the ISR...

Hey! That's exactly the async mode I mentioned in the README. I usually work in the Cosa environment, and it's very easy to hook decode to the ISR. You have to be careful because the fix data is volatile when it's populated by an ISR.

> ...with interrupts re-enabled.

:o You sir, are a square peg. :smiley:

> I'm a little disappointed since your library is so much more sophisticated than mine.

Bahahaha, "sophisticated." Wait, that's not always a good thing. :confused:

Unless you're parsing those bytes later, it sounds like NeoGPS isn't really a good fit. If you do parse the bytes later, you're really just buffering the data like most other libraries. Then you have to add the SRAM and CPU time numbers because you're doing both things: buffering (uGPS) and parsing (NeoGPS).

Well, I certainly appreciate your taking the time to check it out and offer some feedback.

Cheers,
/dev

/dev:

  Serial.print("Memory used: "); Serial.println(2048 - freeMemory());

This is executed after a sentence is decoded, so I'm not sure what it's measuring. And it turns out that string literals use RAM. For a pleasant surprise, I would suggest replacing all the string literals with F("string literal"), at least the ones that are printed. Boy, this is a topic in itself.

Using F() reduces the RAM usage equally (by 110 bytes) between the two sketches. So the difference in RAM usage -- which is what I was trying to measure -- remains the same.

It's true that exactly where you measure the free RAM matters. I wanted to do it in the context of a sketch since that's what counts, ultimately. If I write a memory efficient library that requires lots of external memory then it's a false economy. Likewise with processing speed. Of course the example matters and I chose one that is closer to my needs. A different one could make your approach shine.

/dev:
It really gets down to whether you need the information from the device, or just the bytes. If you are comparing values or calculating a distance, you need the numbers. This is the NeoGPS et al niche.

If you are just logging or displaying the bytes, perhaps in a different order, you only need... bytes. A much simpler problem.

Yes, that's the big difference. It's one reason I didn't just use TinyGPS. All I want, with a few exceptions, is to either display the data or write it as text to an SD card. All of the number crunching I plan to do later on a machine that actually has a DIV instruction and a data path wider than 8 bits.

/dev:
> ...by calling the parse function from the ISR...

Hey! That's exactly the async mode I mentioned in the README. I usually work in the Cosa environment, and it's very easy to hook decode to the ISR. You have to be careful because the fix data is volatile when it's populated by an ISR.

So it is in there? I'll have to take a look. I combined my software serial and gps code because I couldn't figure out the syntax that C++ wanted in order to set up a call from one library to another. I even had an example to work with but I kept getting errors. I'm a C++ moron. So I just glued my two libraries together and planned to deal with it later.

/dev:
> ...with interrupts re-enabled.

:o You sir, are a square peg. :smiley:

Unorthodox perhaps, but I like the fact that in my main loop I can do things that take tens of milliseconds without either breaking them up into multiple steps or using a large RX buffer.

I might be able to parse with interrupts still disabled but I haven't bothered to figure out what the longest path is through my parse function. If that path exceeds the worst case available time between RX interrupts it wouldn't work. At 38400 baud, after accounting for time in interrupts, I think I have something less than 15 microseconds worst case.

/dev:
Well, I certainly appreciate your taking the time to check it out and offer some feedback.

No worries, it was fun. I may have use for your library in the future.

Cheers.