Thursday, May 8, 2014

A sub-second accurate GPS clock

I was able to finish a project from some time ago. I wanted to have a sub-second accurate GPS clock. The idea is that you could stick this clock in the background of something you're filming and get reasonably good and accurate timestamps. Of course, since it's using an LCD display, it's not going to be tremendously good, but for the price, I don't think anyone can complain.

To make it work, you connect up a GPS module of your choice to the RX and TX lines (experiments with SoftwareSerial on other lines were a failure) and the PPS pin to digital pin 2. You'll also need to connect up an AdaFruit RGB LCD shield, or replace LiquidTWI2 with LiquidCrystal and wire the display parallel-style.

This sketch is time-zone and DST aware, so you may need to edit the "summer" and "winter" timezone rule declarations. Depending on your GPS module, you may also need to change the GPS_BAUD or change the interrupt from RISING to FALLING.

#include <Wire.h>
#include <LiquidTWI2.h>
//#include <SoftwareSerial.h>
#include <TinyGPS.h>
#include <Time.h>
#include <Timezone.h>

#define PPS_PIN 2
#define PPS_INT 0
#define RX_PIN 4
#define TX_PIN 3
#define GPS_BAUD 4800

#define LCD_I2C_ADDR 0x20 // for adafruit shield or backpack

LiquidTWI2 display(LCD_I2C_ADDR, 0, 0);
//SoftwareSerial gps_port(RX_PIN, TX_PIN);
#define gps_port Serial
TinyGPS gps;
time_t prevTime = 0; // when the digital clock was displayed
unsigned int prevTenths = 99; // not 0-9
boolean complained = false;
unsigned long last_pps_millis;

/*

For this to work, an extension must be made to the Arduino Time library. This
method's intent is to designate the precise start of a second. It does this by
replacing the prevMillis value saved in the library with the current value of
millis(), but preserving any "owed" updates.

void syncSecond() {
  unsigned long now_millis = millis();
  while (((int)(now_millis - prevMillis)) > 500) { // 500 so we sync to the *nearest* second
    // we're owed at least one update
    now_millis -= 1000;
  }
  prevMillis = now_millis;
}

*/

void pps_interrupt() {
  last_pps_millis = millis();
  syncSecond();
}

void setup() {
  gps_port.begin(GPS_BAUD);
    
  pinMode(PPS_PIN, INPUT);
  attachInterrupt(PPS_INT, pps_interrupt, RISING);
 
  display.setMCPType(LTI_TYPE_MCP23017);
  display.begin(16, 2);
    
  setSyncProvider(gpsTimeSync);
  
  display.setBacklight(WHITE);
  display.print("GPS clock");
 
  delay(2000);
  display.clear();  
}

TimeChangeRule summer = { "PDT", Second, Sun, Mar, 2, -7*60 };
TimeChangeRule winter = { "PST", First, Sun, Nov, 2, -8*60 };
Timezone zone(winter, summer);

time_t gpsTimeSync() {
  unsigned long fix_age = 0;
  gps.get_datetime(NULL, NULL, &fix_age);
  if (fix_age < 2000) {
    unsigned int tenths = ((millis() - last_pps_millis) / 100) % 10;
    tmElements_t tm;
    int year;
    gps.crack_datetime(&year, &tm.Month, &tm.Day, &tm.Hour, &tm.Minute, &tm.Second, NULL, NULL);
    tm.Year = year - 1970;
    time_t out = makeTime(tm);
    if (tenths >= 5) out++; // round to the nearest second given our PPS discipline
    return out;
  }
  return 0;
}

void updateDisplay(time_t Now, unsigned int tenths) {
  tmElements_t tm;
  char buf[16];
  breakTime(Now, tm);
 
  display.setCursor(0, 0);
  sprintf(buf, " %02d:%02d:%02d.%1d %s   ", hourFormat12(Now), tm.Minute, tm.Second, tenths, isPM(Now)?"PM":"AM");
  display.print(buf);
  display.setCursor(0, 1);
  sprintf(buf, "  %2d-%s-%04d   ", tm.Day, monthShortStr(tm.Month), tmYearToCalendar(tm.Year));
  display.print(buf);

}

void loop() {
  while(gps_port.available()) {
    gps.encode(gps_port.read());
  }
  time_t Now = zone.toLocal(now());

  if (timeStatus() != timeNotSet && timeStatus() != timeNeedsSync) {
    unsigned int tenths = ((millis() - last_pps_millis) / 100) % 10;
    if (Now != prevTime || prevTenths != tenths) {
      prevTime = Now;
      prevTenths = tenths;
      complained = false;
      display.setBacklight(GREEN);
      updateDisplay(Now, tenths);
    }
  } else {
    if (!complained) {
      complained = true;
      display.setBacklight(RED);
      display.clear();
      display.print("Waiting for sync");
    }
  }
}

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.