Skip to Content

Arduino + GPS = Lessons and Fun

I mentioned some time back that I am starting to learn electronics from a hobbyist stand point. My lessons have continued and I have turned to learning about GPS in particular. My GPS efforts have taken a bit of a winding road, but I am finally getting things stabilized and can blog about the experience and lessons learned. This is a long posting, covering the story so far...

Part 1

To start with, I had purchased an ArduPilot and a GloablStar EM-406A GPS Module. The idea of this cheap device is to autonomously control a radio controlled airplane. Various sensors can be hooked up as well as a GPS module. My intent was to run this with my R/C truck and only worry about two dimensions. This idea didn't work out as well as I would have liked. Mostly because I had real problems being able to consistently send code to the device. I'm pretty sure this is due to my determination to use Linux for my efforts, and the required programs that were available for my version of Linux (Kubuntu 9.04, at the time) were not quite up to the chore. It may be time to try again though seeing as the various chips are now well supported. In addition to this though was my general failure in getting the GPS running. More on this in a moment.

So, I moved to another cheap alternative - the GPS Shield for the Arduino (more specifically, I have a Freeduino). This cheap circuit board comes pre-set to use the EM-406A module and has a SD Memory Card slot to store data. So with a little bit of soldering (following the great guide at www.ladyada.net) I was all set. Or so I thought. This is where troubleshooting skills came into play. The board seemed to be fine, but no matter what I did I could not get the power light on the GPS module to light up. Without power, it just don't work. It got to the point where I guessed that my module was damaged. Luckily, a friend (John) had one of these modules as well and I was able to borrow it.

Sure enough, the moment I plugged in John's module, everything lit up in a very reassuring manner. I was able to continue with the LadyAda guide and transfer code to the arduino. And everything worked. Data was getting logged to the SD card. I decided to hook up a battery (I built a connector for the 7.2V battery pack I had for the R/C Truck to connect to the arduino), and go for a ride. I didn't get very far.

My phone conversation with John a short while later was great. It went something like this:

Me: "Hey John. Yep, your module worked out of the box."
John: "That's great! You were able to shortcut a lot of troubleshooting".
Me: "Yep. So when you are placing your order for parts at Solarbotics later tonight, include 2 GPS modules."
John: "Sure. You want a spare do ya?"
Me: "No. You need a new module too."
- - - awkward silence - - -
John: "Oh."

Luckily John understands the development process of a mad genius er, I mean hobbyist. He has had similar learning experiences. It also helped that I had immediately offered to replace the module. (we both had replacements within a few days)

So what happened. With the battery hooked up, the circuit powered, I got in the car and put the battery in the "slot" at the top of my dash so it wouldn't move around too much. I stretched the circuit boards to the top of the dash behind the steering wheel where I could keep an eye on it safely. I pulled out, and got all of a few hundred feet away. As I went around a slow corner, the GPS module shifted over top of the pins connecting to the arduino underneath. Those pins include a +5V and a Ground pin very near each other. Oh, and the GPS module has a metal shell. Yep, it appears I shorted out the pins, sending current through the shell, and fried the GPS Module. A nice "zap" type noise, and then 2 or 3 inches of magic smoke leaving the module told me the hole story. I quickly pulled over and yanked the battery. But the damage was done.

But, that experiment was not a total loss. I got a track:

The first bit on the right is the GPS getting a lock, and then experiencing 10 meters or so of error as I moved through the house. Then you see the nice smooth track to the left for a few hundred feet. I had stopped the car within a few seconds of the end of that track, then returned home.

This taught me three very important lessons:

  1. A few cents worth of electrical tape would have saved me a lot of money. The very first thing I did when I got my replacement module was to slap electrical tape over the metal shell. And I now secure the module so it doesn't slide around.
  2. Don't leave your GPS module bouncing around in your tool box unprotected. Keep it in the anti-static bag it comes in. I'm pretty sure this would have saved my first module.
  3. Don't be TOOOO eager to rush out and test things. Take a few moments to think about what can go wrong. Then take reasonable precautions.

Ok, next part....

Part 2

Once I had a replacement GPS Module, I needed to test a few things. One of the projects we're working on at Protospace is to launch a weather balloon with a camera, snap a few pics of the curve of the earth from about 100,000 feet up (approx 30 KM), and then recover the camera. To recover the camera we will be including GPS. (More details at http://wiki.protospace.ca/index.php/Hackerspaces_in_Space.) Two of the issues we need to have some confidence in is how long will a typical battery last, and how much data is likely to be collected in that time. So, I wired up the GPS Shield again (with an ample supply of electrician's tape for safety), and hooked up the battery. This time around I used some elastics to hold things together. The only down side of this arrangement is that it resembles an explosive device to those who are not familiar with the equipment. So after the first trip, I took to carrying this around in a box.

I left this running until the battery didn't have enough juice to power the GPS Module. This took approx 22 hours and logged 4.6 MB of data by logging every second. Yep, you read that right MEGABYTES. On a 2 GIGABYTE memory card. Storage is not an issue. And seeing as our balloon flight should be less than 5 hours (more likely 2 or 3), there is plenty of power. For those interested, the battery produces 1800 mAh at 7.2V. (typical R/C car battery with 6 C cells). The battery is a little heavy for our needs, and we'll probably be using a LiPo battery in the balloon. But the power output/mAh rating is roughly similar.

Irony (or maybe good fortune) struck, and the car power adapter I ordered arrived the day after I started the longevity test, and was beginning to get worried about getting pulled over and questioned about my "bomb". So once the battery died, I didn't need to use the battery anymore. Now the device looks like some simple electronics, but I still put it in a box when I take it out with me.

Now, I just need to do something about WHAT gets logged.

Part 3

Now that I was getting some data, it was time to figure out how to get the real data I need. The default code from LadyAda simply logged the raw GPS data that was received. (aka "NMEA sentences" - take a look at some background info on NMEA data for more details/background.) It turns out that the "RMC" sentence does not include an altitude. To get that, I need to log the "GGA" sentence. However, the GGA sentence does not include the date. It includes the time the reading was taken, but not what day.

I need both the date and altitude for different reasons. I am actually trying to build two projects here. The first is the balloon project mentioned above - we need altitude for this. The second is a cheap GPS logger for my car, where I can leave it running for days or weeks before pulling the memory chip and storing the data to computer for later analysis. So I need to know what day the data was logged. (I have a vague idea to write a system that will examine the GPS data and automagically determine the personal/business usage percentage for my car - the accountants always want to know this...).

So I had two distinct issues I need to tackle. First, I need to be be able to interrogate the GPS data to learn the date, time, location (lat/long), altitude, and other possibly useful data. Next, I need to be able to write the data to storage at will.

My first thoughts on this was to create a file with the date string as it's name (i.e. "290310" - 29 March 2010...). Then I could just create a new file whenever the clock rolled over to a new day and I would know that all data in the file related to that particular day. I later revised this after running into problems creating new files.

For this part of the project, I removed the GPS Shield, and connected the GPS device to the Arduino directly. For logging I used the OpenLog device made by Sparkfun. This device is approximately the size of a quarter (therefore very light), and writes data to a MicroSD card. I know I can do so much more with this device, but my coding kung-fu is not at a sufficient enough level for that (yet!). I tried, but I'm not understanding some parts yet. So, I revised my plan. I'll just create a comma delimited file that has the date, time, location (lat/long), altitude, and other data. Then I can just dump ALL the files on the the device into a database.

So, let me step back and explain the set up I have running now.

First, connecting the GPS to the Arduino. The connecting wire that comes with the module is only about 2 inches long. I had ordered a 12 inch cable to allow me some flexibility in placing the module. Instead, I ended up cutting this cable in half, soldered on some male pins and applies some heat shrink over the pins. Now I had two connectors that allowed me to easily plug the GPS into the female header pins, or onto a bread board. I gave the second half of the connector to my friend John - it was the least I could do after destroying HIS module (See part 1 above for THAT story.)

Now, getting the wiring right is important. Luckily I have found a some resources to aid with this, and more.

  • Basic Positionning - this site shows a nice diagram of how to wire up the GPS Module to a breadboard. The code that is there will not work with an Arduino, but that diagram is very helpful.
  • The EM-406A User Manual (PDF) - this should probably be considered the "definitive" resource.
  • NMEA Reference Manual - You need to understand the NMEA codes/protocols to get the most from your GPS device.
  • SiRF Binary Protocol Reference Manual - You can put the 406A into binary mode and accomplish more with it. I haven't tried this yet - there is a possibility of never getting it out of binary mode... BUT, if you get in this situation, and just cannot get out of it, set your module aside for a couple of weeks. When the internal capacitors discharge the module resets to factory defaults.

So armed with this info I initially connected the RX/TX lines to the TX/RX pins on the Arduino - pins 0 and 1 respectively (the RX from the GPS goes to the TX, and the TX on the GPS goes to the RX). The GND line was plugged into one of the GND pins, and the Vin line was plugged into the 5V pin. The 1PPM line is ignored for now. With this set up I was able to write some simple code to dump what I was receiving:

void setup() {
  Serial.begin(4800);
}
void loop() {
  char c;
  if (Serial.available()) {
    c = Serial.read();
    Serial.print(c);
  }
}

This should start dumping NMEA sentences to your Serial monitor.

But, we need to hook up the RX/TX lines of the OpenLog device, and those pins are now in use by the GPS. To resolve this, I made use of the NewSoftSerial library. This library allows us to assign multiple sets of pins as serial rx/tx pins. To install, download the zip, decompress it, and put the resulting folder in the "libraries" directory of your Arduino-0018 folder. (If using an earlier version, you may need to put it in the hardware/libraries directory).

AND, we need to interpret the NMEA sentences, not just dump them. For this, I made use of the TinyGPS library. Install it with the same instructions mentioned above.

I was trying to do some custom stuff, and failing miserably. The cause of the failures is/was a lack of understanding of the datatypes being used. For instance, I want to build a string from the various elements, and then write the whole string to the file. Well, it turns out that working with strings is NOT simple. I've been spoiled with higher level languages, and now I need to learn basic C coding with regards to strings again. One thing I did learn is that "itoa()", "ultoa()", and "sprintf()" are all available without including any special libraries. I also learned that using ultoa() to convert an "unsigned long" to a string compiles to a smaller package than using sprintf(). And I learned that the sprintf() on the Arduino does not handle float values too well. But I still have a lot to learn in this low level data type management and conversion. Until then, I need to stick with some simplified routines.

Once you have TinyGPS installed, there is an example file you can open (File -> Examples -> TinyGPS -> Examples -> test_with_gps_device). Take some time to get familiar with this file - it's not very tough, shows what the library can do and how to use it, and it serves as the base for my project code (for now).

Next though, we need to hook up the OpenLog device. For this, there are 6 pins (which nicely line up with an FTDI connector). The first two go to Ground (the BLK and GND connections). The next is Volts in., then the TX and RX pins. And finally, the GRN (or RESET) pin. Because we have the GPS and OpenLog requiring Ground and Voltage, we quickly run out of pins on the Arduino. I jumpered the GND and 5V pins to a breadboard, and then connected the GPS and OpenLog via the breadboard. I have some 8 pin Female header pins I'll jury-rig when I want to make this more mobile. For my purposes, I set pins 2 and 3 as the RX/TX pins for the GPS, and pins 4 and 5 for the OpenLog.

NOTE: If you have anything plugged into the default RX/TX pins (pins 0 and 1) when you try to send code to your Arduino, it will fail. Unplug these pins before sending code, then reconnect them after the code is transferred.

Now that I have a mess of wires, I can get to the code. (the mess of wires works though....) Here's the code I'm using (NOTE, my syntax highlighter sometimes mangles the code. Download the code directly to see it in proper detail.) :

#include 
#include 

/*
  This code extracts GPS data and creates a CSV file.
  A GPS device is assumed to be hooked up to pins 2(rx) and 3(tx) at 4800 baud.
  A OpenLog device is assumed to be hooked up to pins 4(rx) and 5(tx) at 9600 baud.  (rate can be changed in the setup() method)
  The NewSoftSerial library is used to provide multiple serial devices.
  The TinyGPS libary is used to interpret the GPS data.

  The default serial pins (0 and 1) have been left un-assigned to avoid problems sending code to the arduino, and/or another serial output
  device can be connected to these.
*/

TinyGPS gps;
NewSoftSerial nss(2, 3);    //change to match the actual pins used
NewSoftSerial logger(4,5);  //change to match the actual pins used

void gpsdump(TinyGPS &gps);
bool feedgps();
void printFloat(double f, int digits = 2);

//Change the log interval to something useful.
//If it is too short you can expect problems, as it takes some time to process the data and spit out the results.
int log_interval = 5000;

void setup()
{
  Serial.begin(4800);
  nss.begin(4800);       //change baud rate as needed.
  logger.begin(9600);    //change baud rate as needed.

  Serial.println("year,month,day,hour,minute,seconds,latitude,longitude,altitude(cm),speed(kmph),course");
  logger.println("year,month,day,hour,minute,seconds,latitude,longitude,altitude(cm),speed(kmph),course");
}

void loop()
{
  bool newdata = false;
  unsigned long start = millis();

  // Every 5 seconds we print an update
  while (millis() - start < log_interval)
  {
    if (feedgps())
      newdata = true;
  }

  if (newdata)
  {
    gpsdump(gps);
  }
}

void printFloat(double number, int digits, bool toLogger = false)
{
  // Handle negative numbers
  if (number < 0.0)
  {
     if (!toLogger) { Serial.print('-'); } else { logger.print('-'); }
     number = -number;
  }

  // Round correctly so that print(1.999, 2) prints as "2.00"
  double rounding = 0.5;
  for (uint8_t i=0; i 0) {
    if (!toLogger) { Serial.print('.'); } else { logger.print('.'); }
  }

  // Extract digits from the remainder one at a time
  while (digits-- > 0)
  {
    remainder *= 10.0;
    int toPrint = int(remainder);
    if (!toLogger) { Serial.print(toPrint); } else { logger.print(toPrint); }
    remainder -= toPrint;
  }
}

void gpsdump(TinyGPS &gps)
{
  long lat, lon, alt;
  float flat, flon, falt, fspeed;
  unsigned long age, date, time, chars;
  int year;
  byte month, day, hour, minute, second, hundredths;
  unsigned short sentences, failed;
  char sfalt[100], sflat[100], sflon[100], sfspeed[100], sfcourse[100];

  gps.get_position(&lat, &lon, &age);
  feedgps(); // If we don't feed the gps during this long routine, we may drop characters and get checksum errors
  gps.f_get_position(&flat, &flon, &age);
  feedgps();
  gps.get_datetime(&date, &time, &age);
  feedgps();
  gps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &hundredths, &age);
  feedgps();
  alt = gps.altitude();
  fspeed = gps.f_speed_kmph();
  feedgps();

  //outputing a comma delimited string in the following format:
  //  year,month,day,hour,minute,second,latitude,longitude,altitude(cm),speed(kmph),course

  //log to file (via the OpenLog device attached as "logger")
  logger.print(year); logger.print(",");
  logger.print(static_cast(month)); logger.print(",");
  logger.print(static_cast(day)); logger.print(",");
  feedgps();
  logger.print(static_cast(hour)); logger.print(",");
  logger.print(static_cast(minute)); logger.print(",");
  logger.print(static_cast(second)); logger.print("."); logger.print(static_cast(hundredths)); logger.print(",");
  feedgps();
  printFloat(flat,5, true); logger.print(",");
  printFloat(flon,5, true); logger.print(",");
  logger.print(alt); logger.print(",");
  feedgps();
  printFloat(gps.f_speed_kmph(), 2, true); logger.print(",");
  printFloat(gps.f_course(), 2, true);
  logger.println();
  feedgps();

  //log to the serial port so we can monitor what is going on
  // --- ERASE this section if serial monitoring is not needed ---
  Serial.print(year); Serial.print(",");
  Serial.print(static_cast(month)); Serial.print(",");
  Serial.print(static_cast(day)); Serial.print(",");
  feedgps();
  Serial.print(static_cast(hour)); Serial.print(",");
  Serial.print(static_cast(minute)); Serial.print(",");
  Serial.print(static_cast(second)); Serial.print("."); Serial.print(static_cast(hundredths)); Serial.print(",");
  feedgps();
  printFloat(flat,5,false); Serial.print(",");
  printFloat(flon,5,false); Serial.print(",");
  Serial.print(alt); Serial.print(",");
  feedgps();
  printFloat(gps.f_speed_kmph(),2,false); Serial.print(",");
  printFloat(gps.f_course(),2,false);
  Serial.println();
  feedgps();
}

bool feedgps()
{
  while (nss.available())
  {
    if (gps.encode(nss.read()))
      return true;
  }
  return false;
}

The code is pretty straight forward, so I won't spend too much time explaining it. This is heavily based on the example code mentioned above, but here's a quick overview and notes about specific changes I have made.

  • First we include the NewSoftSerial and TinyGPS libraries
  • Then we set up the TinyGPS variable, and two Serial variables. The "2,3" and "4,5" correspond to the RX/TX pins (in that order) we'll use for these Serial devices.
  • The next block is just some function prototypes. These functions are defined later in the code.
  • I moved the log_interval value out to a global variable so that it is more visible and we can easily change it later.
  • The setup() function initializes the THREE serial devices we need (the GPS, the OpenLog, and the serial monitor). then we send out the first line to the serial monitor and the logging device. This line is the header row of our CSV file and indicates what data we are collecting.
  • The loop() function sets up a pause (defined by the log_interval value). At the end of that pause we see if we have received a valid GPS sentence. If so, we set a flag to do a data dump. Then we check the flag and call the routine to dump the data.
  • The printFloat() function is needed to handle float values. I have changed this to take a third argument "toLogger". If true, the data is printed to the OpenLog device, otherwise it is sent to the Serial Monitor.
  • The gpsdump() function asks the TinyGPS variable (named "gps" of course!) for various pieces of information. Then it has two blocks to print to the OpenLog device and the serial monitor. The feedgps() function is called often here to make sure we don't loose data and end up with bad sentences while our code is executing. (This may not really be an issue with a five second logging period, but if we drop that period to 1 second, it would become a problem.)
  • The feedgps() function simply checks to see if there is GPS data available, and if so adds it to the "gps" variable. The function returns true if adding this data results in a valid NMEA sentence.

And with that, we have a nice CSV file capturing our GPS data. This has been working on the desktop for short periods of time, but I'm testing longer periods of time now. I will also be wiring it up and taking it on the road as well for further testing. I'm expecting to tweak the code to fine tune it. One immediate improvement I can see is to NOT log data unless we have moved. The margin of error will make that difficult to determine with the lat/long values, but we DO have a speed indicator. It seems that the speed is never more than 1 KM/h while it is sitting still. So, if we find we are moving faster than 1 KM/H it might be safe to assume we are moving and therefore need to log data. I'll need a larger dataset though to confirm this is reasonable.

Here is a sample of the output (copy/pasted from the resulting file):

year,month,day,hour,minute,seconds,latitude,longitude,altitude(cm),speed(kmph),course
2010,3,29,13,25,43.0,50.92594,-128.99281,104980,0.26,266.29
2010,3,29,13,25,49.0,50.92595,-128.99281,105020,0.72,107.86
2010,3,29,13,25,55.0,50.92594,-128.99281,104940,0.59,159.20
2010,3,29,13,26,1.0,50.92594,-128.99279,104850,0.48,151.40
2010,3,29,13,26,7.0,50.92595,-128.99278,104750,0.54,17.55
2010,3,29,13,26,13.0,50.92596,-128.99284,104730,0.72,273.45
2010,3,29,13,26,19.0,50.92597,-128.99282,104590,0.46,304.00
2010,3,29,13,26,25.0,50.92597,-128.99282,104650,0.94,100.69
2010,3,29,13,26,31.0,50.92595,-128.99286,104920,1.26,180.04

(I've mangled the location to put it someplace off the west coast of Canada - otherwise you'd have precise location to my house. I'm sure there are way to many fans out there that would flock to my place.... probably with rifles... :) )

Conclusion

And that is the story so far. Some set backs, some frustrations, some laughs, and more importantly - some success. I hope my trials and tribulations prove useful to others. At the very least I hope to save someone from frying their GPS module like I did.