Friday, January 6, 2012

Arduino-based GPS and barometric altimeter

I've been wanting a GPS data logger to keep track of hikes, but I haven't seen any commercial devices that I like.  For one thing, GPS altitude is not very accurate so I wanted to incorporate a barometric altimeter (which can be very sensitive to short time scale changes in altitude, but has long term drifts due to weather).  I also want to get the raw data spit out by the GPS so I can play with it myself rather than having to go through third-party software.

Fortunately, there is a thriving open-source hardware community with all the answers!  Adafruit sells a kit for a nice Arduino-based GPS shield that logs to an SD card. It interfaces to a device called EM-406 which does all the GPS stuff within a single tiny package and communicates with a simple serial interface.  It's the kind of thing that lives inside most fancy cell phones these days.

I also bought the BMP085 pressure and temperature sensor already mounted on a board (so I wouldn't have to do any surface-mount soldering, with which I have no experience).  The sensor claims a 0.06 hPa pressure error, which could translate into altitude precision of half a meter!  That sounds impressive, we'll have to see how it really performs.

This was my first Arduino project, so there was a lot for me to learn.  The documentation with nice pictures and example code provided by ladyada made it extremely easy to assemble the GPS shield and get to the point of logging GPS data.

So far so good, but then it was time for my customization.  I first wired up the BMP085.  There's just 4 wires to connect and the adafruit GPS shield nicely feeds all the necessary connections through.

The communication with the sensor is done serially using the Arduino's analog 4 and 5 (which is great because it doesn't interfere with the all-digital defaults for the GPS) and the sensor is powered with the 3.3V pin and of course grounded.  Here's what it looks like when I'm done:



Despite my ugly soldering, it works just fine.  There is a nice software library for talking to the BMP085 from adafruit that makes it easy to read the data from this device at the same time as getting the GPS data.

Next I wanted to modify the code to parse the data I want from the GPS and dump it into the same data file as the pressure and temperature data.  This took a bit of hacking because the example code simply dumps the NMEA sentences (the raw data that the GPS module spits out) to the SD card.

I had to look up the format of the various NMEA data chunks.  The EM-406 user guide has everything I needed.  There are several different data modes for the EM-406, which I believe are standards used by different devices.   The GGA data mode contains GPS altitude, which I want to save so I can compare it to pressure altitude later.  GGA does not contain the GPS date, so I'll have to live without that.   I'm also parsing all the individual items from the raw data and logging them separately. Here's my code.

 #include <Wire.h>  
 #include <SD.h>  
 #include <BMP085.h>  
 #include <avr/sleep.h>  
 #include "GPSconfig.h"  
 #include <SoftwareSerial.h>  
 // power saving modes  
 #define SLEEPDELAY 0  
 #define TURNOFFGPS 0  
 #define LOG_RMC_FIXONLY 0  
 // Use pins 2 and 3 to talk to the GPS. 2 is the TX pin, 3 is the RX pin  
 SoftwareSerial gpsSerial = SoftwareSerial(2, 3);  
 // Baud rate of GPS unit  
 #define GPSRATE 4800  
 // Set the pins used   
 #define powerPin 4  
 #define led1Pin 5  
 #define led2Pin 6  
 #define chipSelect 10  
 #define BUFFSIZE 90  
 char buffer[BUFFSIZE];  
 uint8_t bufferidx = 0;  
 uint8_t fix = 0; // current fix data  
 uint8_t i,j,k;  
 char *parseptr;  
 float Temperature;  
 int32_t Pressure;  
 float lat,lon,temp;  
 char date[7];  
 char utc_time[10];  
 char alt[10];  
 File logfile;  
 // BMP085  
 BMP085 bmp;  
 // read a Hex value and return the decimal equivalent  
 uint8_t parseHex(char c) {  
  if (c < '0')  
   return 0;  
  if (c <= '9')  
   return c - '0';  
  if (c < 'A')  
   return 0;  
  if (c <= 'F')  
   return (c - 'A')+10;  
 }  
 // blink out an error code  
 void error(uint8_t errno) {  
  while(1) {  
   for (i=0; i<errno; i++) {  
    digitalWrite(led1Pin, HIGH);  
    digitalWrite(led2Pin, HIGH);  
    delay(100);  
    digitalWrite(led1Pin, LOW);  
    digitalWrite(led2Pin, LOW);  
    delay(100);  
   }  
   for (; i<10; i++) {  
    delay(200);  
   }  
  }  
 }  
 void readline(void) {  
  char c;  
  bufferidx = 0; // start at begninning  
  while (1) {  
   c=gpsSerial.read();  
   if (c == -1)  
    continue;  
   if (c == '\n')  
    continue;  
   if ((bufferidx == BUFFSIZE-1) || (c == '\r')) {  
    buffer[bufferidx] = 0;  
    return;  
   }  
   buffer[bufferidx++]= c;  
  }  
 }  
 void setup() {  
  WDTCSR |= (1 << WDCE) | (1 << WDE);  
  WDTCSR = 0;  
  Serial.begin(9600);  
  Serial.println("\r\nGPS and BMP085 logger");  
  pinMode(led1Pin, OUTPUT);  
  pinMode(led2Pin, OUTPUT);  
  pinMode(powerPin, OUTPUT);  
  digitalWrite(powerPin, LOW);  
  // make sure that the default chip select pin is set to  
  // output, even if you don't use it:  
  pinMode(10, OUTPUT);  
  // see if the card is present and can be initialized:  
  if (!SD.begin(chipSelect)) {  
   Serial.println("Card init. failed!");  
   error(1);  
  }  
  strcpy(buffer, "GPSLOG00.TXT");  
  for (i = 0; i < 100; i++) {  
   buffer[6] = '0' + i/10;  
   buffer[7] = '0' + i%10;  
   // create if does not exist, do not open existing, write, sync after write  
   if (! SD.exists(buffer)) {  
    break;  
   }  
  }  
  logfile = SD.open(buffer, FILE_WRITE);  
  if( ! logfile ) {  
   Serial.print("Couldnt create ");   
   Serial.println(buffer);  
   error(3);  
  }  
  Serial.print("Writing to ");   
  Serial.println(buffer);  
  logfile.println("# A GPS track data file");  
  logfile.println("# UTC time  lat  lon  gps_alt  pressure temperature");  
  logfile.println("# hhmmss.sss deg  deg  m     hPa    C");  
  logfile.println("#");  
  // connect to the GPS at the desired rate  
  gpsSerial.begin(GPSRATE);  
  Serial.println("Ready!");  
  gpsSerial.print(SERIAL_SET);  
  delay(250);  
  gpsSerial.print(GGA_ON);  
  delay(250);  
  //gpsSerial.print(GLL_OFF);  
  //delay(250);  
  //gpsSerial.print(GSA_OFF);  
  //delay(250);  
  //gpsSerial.print(GSV_OFF);  
  //delay(250);  
  gpsSerial.print(RMC_OFF);  
  delay(250);  
  //gpsSerial.print(VTG_OFF);   
  // gpsSerial.print(RMC_ON);  
  // delay(250);  
  gpsSerial.print(WAAS_ON);  
  bmp.begin();   
 }  
 void loop() {  
  char c;  
  uint8_t sum;  
  char *p;  
  // read a line of data  
  readline();  
  Serial.print("raw line: ");  
  Serial.println(buffer);  
  // get checksum  
  sum = parseHex(buffer[bufferidx-2]) * 16;  
  sum += parseHex(buffer[bufferidx-1]);  
  Serial.print("Checksum:");  
  Serial.println(sum);  
  // check checksum  
  for (i=1; i < (bufferidx-3); i++) {  
   sum ^= buffer[i];  
  }  
  if (sum != 0) {  
   //putstring_nl("Cxsum mismatch");  
   Serial.print('~');  
   bufferidx = 0;  
   return;  
  }  
  p = buffer;  
  // UTC time is item 2  
  p = strchr(p, ',')+1;  
  for (k=0;k<9;k++)  
   utc_time[k]=p[k];  
  Serial.print(" UTC time:");  
  Serial.println(utc_time);  
  // lat is item 3  
  p = strchr(p, ',')+1;  
  // get degrees and convert minutes to decimal degrees  
  lat = (p[0]-'0')*10.0 + (p[1]-'0') + ((p[2]-'0')*10.0 + (p[3]-'0') + (p[5]-'0')/10.0 + (p[6]-'0')/100.0 + (p[7]-'0')/1000.0 + (p[8]-'0')/10000.0)/60.0;  
  // N or S is item 4  
  p = strchr(p, ',')+1;  
  if (p[0]=='S')  
   lat *= -1.0;  
  Serial.print(" latitude:");  
  Serial.println(lat,7);  
  // lon is item 5  
  p = strchr(p, ',')+1;  
  // get degrees and convert minutes to decimal degrees  
  lon = (p[0]-'0')*100.0 + (p[1]-'0')*10.0 + (p[2]-'0') + ((p[3]-'0')*10.0 + (p[4]-'0') + (p[6]-'0')/10.0 + (p[7]-'0')/100.0 + (p[8]-'0')/1000.0 + (p[9]-'0')/10000.0)/60.0;  
  // E or W is item 6  
  p = strchr(p, ',')+1;  
  if (p[0]=='W')  
   lon *= -1.0;  
  Serial.print(" longitude:");  
  Serial.println(lon,7);  
  // fix indicator is item 7  
  p = strchr(p, ',')+1;    
  if (p[0]=='0') {  
   fix = 0;   
   digitalWrite(led1Pin, LOW);  
  }  
  else {  
   fix = 1;  
   digitalWrite(led1Pin, HIGH);  
  }  
  // alt is item 10  
  p = strchr(p, ',')+1;  
  p = strchr(p, ',')+1;  
  p = strchr(p, ',')+1;  
  j=0;  
  while (p[j] != ',') {  
   alt[j] = p[j];   
   j++;  
  }  
  Serial.print(" alt:");  
  Serial.println(alt);  
  bufferidx = 0;  
  // now start logging data  
  digitalWrite(led2Pin, HIGH);   // sets the digital pin as output  
  // write date  
  logfile.print(date);  
  logfile.print(" ");  
  // write UTC time  
  logfile.print(utc_time);  
  logfile.print(" ");  
  // write latitude  
  logfile.print(lat,7);  
  logfile.print(" ");  
  // write longitude  
  logfile.print(lon,7);  
  logfile.print(" ");  
  // write altitude  
  logfile.print(alt);  
  logfile.print(" ");  
  // get data from the BMP085 and log  
  Temperature = bmp.readTemperature();  
  Serial.print("Temperature = ");  
  Serial.print(Temperature);  
  Serial.println(" C");  
  Pressure = bmp.readPressure();    
  Serial.print("Pressure = ");  
  Serial.print(Pressure);  
  Serial.println(" Pa");   
  // write pressure  
  logfile.print(" ");  
  logfile.print(Pressure);  
  logfile.print(" ");  
  // write temperature  
  logfile.println(Temperature);     
  logfile.flush();   
  digitalWrite(led2Pin, LOW);  
  // turn off GPS module?  
  if (TURNOFFGPS) {  
   digitalWrite(powerPin, HIGH);  
  }  
  delay(SLEEPDELAY * 1000);  
  digitalWrite(powerPin, LOW);  
  return;  
 }  
http://codeformatter.blogspot.com/2009/06/about-code-formatter.html

Finally, I wanted to make a nice package for the device and set it up to run off a battery pack so I can use this device remotely.  On the adafruit website, they recommend putting it inside a fancy Otterbox, which seals very tightly and is extremely strong and completely waterproof, but it would complicate things for me since I want to measure the outside pressure.

So I settled on a nice $2 sandwich box for now.  It's solid and has plenty of room for all my extra stuff (maybe even a sandwich too!).  It won't be completely rain proof, but who hikes in the rain anyway?  Also since it doesn't hermetically seal I don't have to worry about mounting the pressure sensor on the outside of the box.  The temperature won't perfectly reflect the temperature of the outdoors since it's in the box with all the electronics, but for now I won't worry about it.  I also found a nice pull-chain on-off switch.

It's easy to use a different power source for the Arduino - it can handle a 9V battery.  I got a 2.1mm plug and connected it to a pull-chain switch pack that plugs into power jack on the Arduino.  I'm not sure how long an alkaline 9V battery will last running the BMP085 and the GPS and logging data every second - that will have to wait for tests.  If it seems too short, I can try slowing down the logging rate.

Et voila:

Now I'm looking forward to field testing!

1 comment:

  1. Hi, cool project! How did the waterproof casing affect the altimeter?

    ReplyDelete