MTK3339 chipset GPS Display

I have recently been intrigued by some really innovative geo-caching and GPS-enabled Arduino projects, and it got the creative juices flowing. I read up on various GPS modules on the market and settled on a GPS module called the “Ultimate GPS Breakout v.3″ that sported the highest receiver sensitivity (-165 dBm), and the fastest read data rate (10 Hz), all packaged in an Arduino-friendly (5V interface) module. Adafruit has a GPS library for this chipset and a whole suite of example code to get you going quickly.

There were several sellers on eBay and I got mine for $37.  As I had a battery-driven project in mind which probably required a solid enclosure, I wasn’t sure whether I’d need more than the on-board aerial. So I also purchased a u.FL to SMA pigtail ($3) and an magnetic base active antenna (5$). This will offer additional gain to ensure operation at very low signal strengths, although it may turn out to be unnecessary. For $8 it’s worth the piece of mind.

GPS module

“Ultimate GPS Breakout”

UFL - SMA pigtail

u.FL – SMA pigtail

GPS Active Aerial

GPS Active Aerial

 

 

 

 

 

The module arrived late last week and as today is a rainy Saturday, it’s the perfect opportunity to play!

I connected the module to an Arduino Nano and a 4×20 LCD display. The unit operates at 9600 baud and connects to a serial port on the Arduino. I used the soft.serial module so that I could still debug through the IDE terminal. I went through several of the examples offered by Adafruit to get a feel for the operation of the board. The examples proved useful for a base upon which to add the LCD display drivers. The unit started up and operated immediately, spewing out strangely formatted data strings. After a couple of minutes it got it’s ‘fix’ by connecting to several (up to 10 at one point) satellites, despite the fact that a) I was inside and b) it was raining pretty heavily. Spectacular!  I am looking forward to seeing the module’s performance with the active antenna – currently winging its way to me care of China Post!

The data from the MTK3339 chipset is formatted according to the National Marine Electronics Association (NMEA) protocols.  I found an article explaining the NMEA data protocols very useful to understand the bit streams (I have embedded the pertinent information into the header of the Arduino file). Once I had the raw data and formatted it correctly, I plugged the location data into Google maps… et voila… The location it reported was spot on!  I also now know that I am approximately 163m above average sea level, and, according to the unit, traveling at 0.04 metres per second!  Of course, the GPS receiver spits out the time and date (synchronized to GMT) so there’s another possibility (albeit an expensive one) to build it into a clock! The chipset also includes an RTC and a small battery holder for a CR1220 coin cell to keep the RTC operating in the absence of power.

Anyway, I’m delighted with the initial set-up!  In ‘normal’ mode, the display shows the following data: time and date; location [with number of satellites being tracked] as latitude, longitude (in decimal to 4 dp); altitude above mean sea level (metres) and speed (which I have converted from knots to metres / second). The Arduino software polls the GPS module and updates the display every couple of seconds.  I have also added routines to calculate both distance and heading between two way-points – the GPS’s current position and various places where family members live (Ottawa, Montreal, Vancouver, Cardiff and Portsmouth). A three-position (centre-off) toggle switches between a) normal data presentation mode, b) rotating screens that display relative distances and headings to these locations and c) a “walk-about” mode that shows relative distance and heading to home position.

The code also spits out the data from the GPS streams, in a more verbose manner, to the serial port so that you can view the data on the IDE terminal. A software switch also enables the output of the raw GPS data strings (PGTOP, GGA and RMC).

So far, this has proven to be accurate to around 5m although with the active antenna, I hope that the resolution will increase slightly.

GPS Display

GPS Display – Showing module (top centre) and Arduino Nano

Now that I have a ‘GPS platform’ upon which to build, I’ll have to get my thinking cap on. I’ve already thought about a simple way-point based hunt but that’s been done before… “It’s so deivative!” I hear you say!

Will need to sleep on it.

Here is the latest Arduino code that includes the switch and multiple displays…

[codesyntax lang=”php” title=”GPS Display” blockstate=”collapsed”]

/*******************************************************************************************
 * /                                    GPS Display System
 * /                                   Adrian Jones, Oct. 2014
 * /
/********************************************************************************************/

// Based on test code for Adafruit GPS modules using MTK3329/MTK3339 driver
// For more information, please see http://www.adafruit.com/products/746
// 
// Build 1
//   r1 141025 - initial build with MTK3339 chipset GPS module (“Ultimate GPS Breakout v.3″) & 4x20 LCD
//   r2 141027 - added more info from parsed output, distance & heading to Fairmont
//   r3 141028 - multiple displays to Sian, Samuel, Dad and town
//   r4 141029 - added switch for display settings

//********************************************************************************************
#define build 1
#define revision 4
//********************************************************************************************

// GPS Data Strings
// PGTOP - antennae status
// e.g.: $PGTOP,11,2*6E
// a. Antennae: 1 - short/problem, 2 - internal, 3 - external
// b: Checksum = *6E

// GGA - essential fix data which provide 3D location and accuracy data:
// e.g.: $GPGGA,191258.000,4539.7024,N,07538.5915,W,1,10,0.86,167.6,M,-34.1,M,,*51
// a. Time: 191258.000 = 19:12:58 GMT  + 000 ms
// b. Location: 4539.7024,N,07538.5915,W = 44° 39.6142' (N), -075° 38.5915' (W)
// c. Fix quality: 0 - invalid, 1 - GPS fix (SPS)
// d. Satellites tracked = 10
// e. Horizontal dilution of position: 0.86 ??
// f. Altitude: 167.6,M = 167.6 meters above mean sea level
// g. Height of geoid (mean sea level) above WGS84 ellipsoid = -34.1,M
//    (empty field) time in seconds since last DGPS update
//     (empty field) DGPS station ID number
// h. Checksum: *51

//RMC - The Recommended Minimum, essential gps pvt (position, velocity, time) data:
// e.g: $GPRMC,191258.000,A,4539.7024,N,07538.5915,W,0.32,159.17,251014,,,A*78
// a. Time: 191258.000 = 19:12:58 GMT  + 000 ms
// b. Fix status: A - active (lock), V - void (invalid)
// c. Location: 4539.7024,N,07538.5915,W = 44° 39.6142' (N), -075° 38.5915' (W)
// d. Speed: 0.32 = 0.32 knots
// e. Tracking Angle: 159.17 = 159.17° in degrees True
// f. Date: 251014 = 25 October 2014
// g. Magnetic Variation: (e.g. 003.1,W)
// h. Checksum: *78 (always begins with *)


//********************************************************************************************
// ARDUINO NANO PIN USE SUMMARY
// GPS: TX-D3, RX-D2; LCD: rs-A5, en-A4, d4-A3, d5-A2, d6-A1, d7-A0; Switches: sw1-D4,sw2-D5
//********************************************************************************************


#include <Adafruit_GPS.h>
#include <SoftwareSerial.h>

// GPS module (Vcc -> 5V, Gnd -> Gnd, TX -> D3, RX -> D2
SoftwareSerial GPSserial(3, 2);        // GPS TX, GPS RX
Adafruit_GPS GPS(&GPSserial);

#define GPSECHO  false                 // echo raw GPS data to the Serial console
boolean serEn = true;                  // serial enable
void useInterrupt(boolean);            // Func prototype keeps Arduino 0023 happy
unsigned long timer = millis();        // interrupt timer
unsigned long loopTime;                 // interrupt timer
#define loopTimeLong 3000              // update loop time (long)
#define loopTimeShort 1500             // update loop time (short)

#define GMToffset -4                   // time offset to GMT

// LCD
#include <LiquidCrystal.h>
LiquidCrystal lcd(A5, A4, A3, A2, A1, A0); // initialize LCD pins lcd(rs, en, d4, d5, d6, d7)
byte mps[8] = {0x1F,0x15,0x11,0x0E,0x08,0x0E,0x02,0x0E};  // meters per second icon
byte deg[8] = {0x06,0x09,0x09,0x06,0x00,0x00,0x00,0x00};  // degrees icon
boolean lcdEn = true;                   // lcd enable


boolean d1 = false;                     // display window
#define cottLat 45.66160                 // cottage latitude
#define cottLon -75.64318                // cottage longitude
#define fairLat 45.4008046              // 116 Fairmont latitude
#define fairLon -75.7212937             // 116 Fairmont longitude
#define sianLat 45.4794974              // Sian latitude
#define sianLon -73.5659134             // Sian  longitude
#define samLat 49.2455056               // Samuel latitude
#define samLon -123.0938175             // Samuel longitude
#define dadLat 51.5122139               // Dad latitude
#define dadLon -3.2280636               // Dad longitude
#define alunLat 50.8984057              // Alun latitude
#define alunLon -1.0045349              // Alun longitude

int pages=6;
int page=0;
char* ptxt[]   = {"Cottage","Fairmont","Sian", "Samuel","Dad","Alun"};
float lat[6]   = {cottLat,fairLat,sianLat,samLat,dadLat,alunLat};
float lon[6]   = {cottLon,fairLon,sianLon,samLon,dadLon,alunLon};

float distance;                          // calculated distance
float heading;                           // calculated heading

// switch
#define sw1p  4                          // switch 1 pin
#define sw2p  5                          // switch 2 pin
boolean swEn = true;                     // switch enable
boolean sw1 = false;                     // switch 1 state enable
boolean sw2 = false;                     // switch 2 state enable


//*****************************************************************************************//
//                                      Initial Setup
//*****************************************************************************************//
void setup() {

  if(serEn) {
    Serial.begin(115200);
    Serial.println(F("GPS Data Testing: Adrian Jones, October 2014"));
    Serial.print(F("Build ")); 
    Serial.print(build); 
    Serial.print(F(".")); 
    Serial.println(revision);
    Serial.print(F("Free RAM: "));  
    Serial.print(freeRam()); 
    Serial.println(F("B"));
  }

  if(swEn) {
    pinMode(sw1p,INPUT_PULLUP); digitalWrite(sw1p,HIGH);
    pinMode(sw2p,INPUT_PULLUP); digitalWrite(sw2p,HIGH);
  }

  GPS.begin(9600);                              // default baud rate of GPS module
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA); // RMC (recommended minimum) and GGA (fix data) including altitude
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ);    // 1 Hz update rate
  GPS.sendCommand(PGCMD_ANTENNA);               // Antenna status

  if(lcdEn) {
    lcd.begin(20, 4);                           // setup LCD  for 16 columns and 2 rows
    lcd.createChar(0, mps);
    lcd.createChar(1, deg);
    lcd.setCursor(0, 0);
    lcd.print(F("GPS Display System"));   
    lcd.setCursor(0, 1); 
    lcd.print(F("Free RAM: "));  
    lcd.print(freeRam()); 
    lcd.print(F("B")); 
    delay(2000);
    lcd.setCursor(0, 1); 
    lcd.print(F("    .. ready ..    "));
  }

  useInterrupt();
  delay(1000);
  GPSserial.println(PMTK_Q_RELEASE);
  loopTime = loopTimeLong;
}

//*****************************************************************************************//
//                                      Interupt Routine
//*****************************************************************************************//
void useInterrupt() {
    OCR0A = 0xAF;
    TIMSK0 |= _BV(OCIE0A);
}

//*****************************************************************************************//
//                                      MAIN LOOP
//*****************************************************************************************//
void loop() {                   // run over and over again

  doSwitchState();

  if (GPS.newNMEAreceived()) {  // if a sentence is received, we can check the checksum, parse it...
    if (!GPS.parse(GPS.lastNMEA() ))   // this also sets the newNMEAreceived() flag to false
      return;                      // we can fail to parse a sentence in which case we should just wait for another
  }
  if (timer > millis())  timer = millis();  // if millis() or timer wraps around, we'll just reset it
  if (millis() - timer > loopTime) {            // approximately every 2 seconds or so, print out the current stats
    timer = millis(); // reset the timer
    if(serEn) {   
      Serial.print(F("\nTime: "));
      Serial.print(GPS.hour, DEC); 
      Serial.print(F(":"));
      Serial.print(GPS.minute, DEC); 
      Serial.print(F(":"));
      Serial.print(GPS.seconds, DEC); 
      Serial.print(F("."));
      Serial.println(GPS.milliseconds);        // long word
      Serial.print(F("Date: "));
      Serial.print(GPS.day, DEC); 
      Serial.print(F("/"));
      Serial.print(GPS.month, DEC); 
      Serial.print(F("/20"));
      Serial.println(GPS.year, DEC);
      Serial.print(F("Fix: ")); 
      Serial.print((int)GPS.fix);
      Serial.print(F(" Quality: ")); 
      Serial.println((int)GPS.fixquality); 
    }

    if(lcdEn) {
      lcd.setCursor(0, 1); 
      lcd.print(F("T:")); 
      int hr = (24 + GPS.hour + GMToffset)%24; 
      if(hr<10) lcd.print(F("0")); 
      lcd.print(hr, DEC);    
      lcd.print(F(":"));
      if(GPS.minute<10)  lcd.print(F("0")); 
      lcd.print(GPS.minute, DEC);  
      lcd.print(F(":"));
      if(GPS.seconds<10) lcd.print(F("0"));  
      lcd.print(GPS.seconds, DEC); 
      lcd.print(F(" "));
      lcd.print(GPS.day, DEC);  
      lcd.print(F("/"));
      lcd.print(GPS.month, DEC); 
      lcd.print(F("/"));
      lcd.print(GPS.year, DEC);
    }

    if (GPS.fix) {
      if(serEn) {
        Serial.print(F("Location: "));
        Serial.print(GPS.latitude, 4); 
        Serial.print(GPS.lat);
        Serial.print(", "); 
        Serial.print(GPS.longitude, 4); 
        Serial.println(GPS.lon);
        Serial.print(F("Location (in degrees): "));
        Serial.print(GPS.latitudeDegrees, 4);
        Serial.print(F(", ")); 
        Serial.println(GPS.longitudeDegrees, 4);
        Serial.print(F("Speed (knots): ")); 
        Serial.println(GPS.speed);
        Serial.print(F("Angle: ")); 
        Serial.println(GPS.angle);
        Serial.print(F("Altitude: ")); 
        Serial.println(GPS.altitude);
        Serial.print(F("Satellites: ")); 
        Serial.println((int)GPS.satellites);
        Serial.print(F("Height of geoid: ")); 
        Serial.println(GPS.geoidheight, 2);
        Serial.print(F("Horizontal dilution of position: ")); 
        Serial.println(GPS.HDOP, 2);
        Serial.print(F("Magnetic Variation: ")); 
        Serial.print(GPS.magvariation, 2);
        Serial.println(GPS.mag);     
      }
      if(lcdEn) {
        if( sw1 && page == 0 || (!sw1 && !sw2) ) {      // if sw1 ON and initial page, or both sw1 and sw2 are OFF then...
          clearline(0, 2);
          lcd.print(F("L:")); lcd.print(GPS.latitudeDegrees, 5); lcd.print(F(",")); lcd.print(GPS.longitudeDegrees, 5);

          clearline(0, 3);
          lcd.print(F("S:")); lcd.print(int(GPS.satellites)); lcd.print(F(" A:")); lcd.print((int) GPS.altitude); lcd.print(F("m ")); 
          lcd.print(F("V:")); lcd.print((float)(GPS.speed *0.514 )); lcd.write(byte(0)); lcd.print(F(" "));
        } 
        if(sw1 && page > 0) {
          clearline(0, 1); 
          lcd.print(F("Relative to ")); lcd.print(ptxt[page]); lcd.print(F(":"));
          distance = calc_dist( (float)lat[page], (float)lon[page], GPS.latitudeDegrees, GPS.longitudeDegrees );
          heading  = calc_head( (float)lat[page], (float)lon[page], GPS.latitudeDegrees, GPS.longitudeDegrees );
          clearline(0, 2); 
          lcd.print(F("Dist: ")); lcd.print(distance,2);  lcd.print(F("km"));
          clearline(0, 3);
          lcd.print(F("Head: "));  lcd.print(heading,1);   lcd.write(byte(1));          
        }
        if(sw1) page = (++page)%pages;
        
        if(sw2) {
          clearline(0, 1); 
          lcd.print(F("Relative to ")); lcd.print(ptxt[0]); lcd.print(F(":"));
          distance = calc_dist( (float)lat[0], (float)lon[0], GPS.latitudeDegrees, GPS.longitudeDegrees ) * 1000;
          heading  = calc_head( (float)lat[0], (float)lon[0], GPS.latitudeDegrees, GPS.longitudeDegrees );
          clearline(0, 2); 
          lcd.print(F("Dist: ")); lcd.print(distance,1);  lcd.print(F("m"));
          clearline(0, 3);
          lcd.print(F("Head: "));  lcd.print(heading,1);   lcd.write(byte(1));          
        }
      }     
    } 
    else {
      if (lcdEn) {
        lcd.setCursor(0, 1); 
        lcd.print(F("RX satellites ["));
        lcd.print((int)GPS.satellites);
        lcd.print(F("] "));
      }
    }   
  }
}

void clearline(byte col, byte row) {
  lcd.setCursor(col,row);
  lcd.print(F("                    "));
  lcd.setCursor(col,row);
}


// ********************************************************************************** //
//                                      SUBROUTINES
// ********************************************************************************** //
// Interrupt is called once a millisecond, looks for any new GPS data, and stores it
SIGNAL(TIMER0_COMPA_vect) {
  char c = GPS.read();
  // if you want to debug, this is a good time to do it!
  if (GPSECHO)
    if (c) UDR0 = c;  // writing direct to UDR0 faster than Serial.print; only one character at a time. 
}

// ********************************************************************************** //
//                                      OPERATION ROUTINES
// ********************************************************************************** //
// FREERAM: Returns the number of bytes currently free in RAM  
int freeRam(void) {
  extern int  __bss_end, *__brkval; 
  int free_memory; 
  if((int)__brkval == 0) {
    free_memory = ((int)&free_memory) - ((int)&__bss_end); 
  } 
  else {
    free_memory = ((int)&free_memory) - ((int)__brkval); 
  }
  return free_memory; 
}


//*************************************************************************
//   Functions to calculate the distance and heading between two waypoints
//         Extracted from http://letsmakerobots.com/node/19554 
//*************************************************************************/
float calc_dist(float flat1, float flon1, float flat2, float flon2) {
float dist_calc=0;
float dist_calc2=0;
float diflat=0;
float diflon=0;

//I've to split all the calculation in several steps. If i try to do it in a single line the arduino will explode.
diflat=radians(flat2-flat1);
flat1=radians(flat1);
flat2=radians(flat2);
diflon=radians((flon2)-(flon1));

dist_calc = (sin(diflat/2.0)*sin(diflat/2.0));
dist_calc2= cos(flat1);
dist_calc2*=cos(flat2);
dist_calc2*=sin(diflon/2.0);
dist_calc2*=sin(diflon/2.0);
dist_calc +=dist_calc2;

dist_calc=(2*atan2(sqrt(dist_calc),sqrt(1.0-dist_calc)));

dist_calc*=6371.0; //Converting to km
return dist_calc;
}

float calc_head(float flat1, float flon1, float flat2, float flon2) {
  flat1 = radians(flat1);
  flat2 = radians(flat2);
  flon1 = radians(flon1); 
  flon2 = radians(flon2);
  heading = atan2(sin(flon2-flon1)*cos(flat2),cos(flat1)*sin(flat2)-sin(flat1)*cos(flat2)*cos(flon2-flon1)),2*PI;
  heading = heading*180/PI;  // convert from radians to degrees
  int head = heading; //make it a integer now
  if(head<0) heading+=360;   //if the heading is negative then add 360 to make it positive
  return heading;
}


void doSwitchState() {
  sw1 = !digitalRead(sw1p);
  sw2 = !digitalRead(sw2p);
  loopTime = (sw1 && !sw2)? loopTimeLong : loopTimeShort;
}

[/codesyntax]

Here is the schematic of the design…

GPS Display: Design Schematic

GPS Display: Design Schematic

As the creative juices start to digest this new experience with the module, I suspect that several more GPS-enabled projects will grace this site. Any and all thoughts are sought and welcomed. Bye the way, you may notice that in the image above the display actually shows my current location (although I now display it to 4 dps).

So, family aside, can anyone work out my postal address?

Just asking…

One thought on “MTK3339 chipset GPS Display

Come on... leave a comment...