GPS Satellite module - Accuracy when fix is lost

I'm currently building a satellite clock based on a module which has a label "ubox Neo 6M" stuck on it and I am trying to understand the validity of the time stamps it delivers under circumstances of limited satellite coverage.

When it first starts, it delivers only empty NMEA sentences.

After a certain period it appears to deliver partly populated $GPRMC messages which contain time and stamps and the Data Status field is set to 'V' (navigation receiver warning) . It appears that this happens after 2 or more satellites have come into view but that is not totally clear to me.

After a fix has been achieved, with more satellites coming into view, the $GPRMC messages are fully populated and the Data status field is set to 'A' (active).

If the fix is subsequently lost the Data status field reverts to 'V'.

That is all more or less clear to me. The question is what happens next if the number of satellites in view further diminishes? The module appears to continue to deliver these messages. I'm guessing that at a particular point, these messages can no longer supported by supported by a satellite (or satellites) and are derived wholly from the module's own oscillator.

So, the question is, is there an easy way to determine the status of a timestamp when the Data Status is set to 'V' (navigation receiver warning)?

The reason I want to understand this better is that I will use a real time clock to handle the start up during a power failure. However, there may also be a point at which the RTC would deliver more accurate results that the GPS module with data status "warning"

Sample from  http://aprs.gids.nl/nmea/

$GPRMC

Recommended minimum specific GPS/Transit data

eg1. $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62
eg2. $GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68


           225446       Time of fix 22:54:46 UTC
           A            Navigation receiver warning A = OK, V = warning
           4916.45,N    Latitude 49 deg. 16.45 min North
           12311.12,W   Longitude 123 deg. 11.12 min West
           000.5        Speed over ground, Knots
           054.7        Course Made Good, True
           191194       Date of fix  19 November 1994
           020.3,E      Magnetic variation 20.3 deg East
           *68          mandatory checksum


eg3. $GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70
              1    2    3    4    5     6    7    8      9     10  11 12


      1   220516     Time Stamp
      2   A          validity - A-ok, V-invalid
      3   5133.82    current Latitude
      4   N          North/South
      5   00042.24   current Longitude
      6   W          East/West
      7   173.8      Speed in knots
      8   231.8      True course
      9   130694     Date Stamp
      10  004.2      Variation
      11  W          East/West
      12  *70        checksum


eg4. $GPRMC,hhmmss.ss,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,ddmmyy,x.x,a*hh
1    = UTC of position fix
2    = Data status (V=navigation receiver warning)
3    = Latitude of fix
4    = N or S
5    = Longitude of fix
6    = E or W
7    = Speed over ground in knots
8    = Track made good in degrees True
9    = UT date
10   = Magnetic variation degrees (Easterly var. subtracts from true course)
11   = E or W
12   = Checksum

I dont think the nmea website will define the exact behaviour of all different models of GPS.

I think you will need to study the datasheet for the "ubox Neo 6M" to determine its exact behaviour in relation to how and when it indicates it has time sync.

If the GPS does not have an actual timesync, I wonder how it could know how accurate its internal clock\time now is ?

Also note that the accuracy of the timesync pulse itself, down to nS, is down to postion accuracy as well, and there is no definative way of knowing what the position accuracy is, unless you know your actual position. If you do have an accurate survey of position then I recall that with the Ublox GPSs you can use that to correct the timesync pulse.

My approach would be to light a warning LED on the clock, any time GPS sentences are received that lack the "A" status.

When the A status is restored, update the time and douse the LED.

OK. Thanks. This is effectively what I show at the moment.
If it is a new start and the RTC has a plausible time, I show that marked "R" in the LCD display.
As soon as I get status "V" from the GPS module, I show that as the marker.
When an "A" comes, I show that as the marker.

The dilemma comes next, when it reverts to "V", and is really as question of which to trust. The RTC or the Neo module. I think, for the moment, I'll do it like this. If the Neo has 1 or more satellites in view, I'll take that and allow it to update the RTC. If zero satellites are in view, I'll use the RTC to display the time.

I've looked in the Neo 6M data sheet and it really does not seem comprehensive enough to answer this completely.

Most modules require a "fix" before valid sentences are output. With my 8 YO Neo6, time is still valid even when the fix is lost. I use a personal hack of the Adafruit GPS lib and parsing only RMC sentence.

https://gpsd.gitlab.io/gpsd/NMEA.html#_dates_and_times

Added:

The GPRMC log outputs these messages without waiting for a valid almanac. Instead, it uses a UTC time, calculated with default parameters. In this case, the UTC time status (see the TIME log) is set to WARNING since it may not be one hundred percent accurate. When a valid almanac is available, the receiver uses the real parameters. Then the UTC time status is set to VALID.

https://docs.novatel.com/OEM7/Content/Logs/GPRMC.htm

It is extremely easy to just capture the GPRMC sentence from a GPS module and retransmit the sentence over UDP for home use. My rather old TartisTime is based on ESP8266:

/*
   GPS Portal, part of Tardis Time Server by: M. Ray Burnette 20150912
   Create a private 10. network and capture all DNS requests to the Time Portal
   Respond to both UDP and HTML requests
   Arduino GUI 1.6.7 on Linux Mint 17.3 tested on 20160203
    Sketch uses 246,644 bytes (56%) of program storage space. Maximum is 434,160 bytes.
    Global variables use 43,748 bytes (53%) of dynamic memory, leaving 38,172 bytes for local variables. Maximum is 81,920 bytes.

   ESP8266 core: http://arduino.esp8266.com/staging/package_esp8266com_index.json
*/

#include <Streaming.h>                                          // \Documents\Arduino\libraries\Streaming (legacy)
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <ESP8266WebServer.h>
#include <DNSServer.h>
#include "./Utility.h"                                          // See Notes tab for credits

const byte   DNS_PORT  =   53;                                  // Capture DNS requests on port 53
int          ack_Count =    0;
uint8_t      hour, minute, seconds;                             // hour, minure, seconds,
uint8_t      day, month, year;                                  // year, month, day;
unsigned int localPort = 8888;                                  // any unused port on LAN
IPAddress    apIP(10, 10, 10, 1);                               // Private network address: local & gateway

char         packetBuffer[UDP_TX_PACKET_MAX_SIZE];              // buffer to hold incoming packet,
char         ReplyBuffer[] = "acknowledged";                    // a 12 character string to send back

String       responseHTML = ""
                            "<!DOCTYPE html><html><head><title>CaptivePortal</title></head><body>"
                            "<h1>Tardis Time</h1><p>Coordinated Universal Time (UTC) "
                            "HH:MM:SS  YYYYMMDD </p></body></html>";

DNSServer         dnsServer;                                    // Create the DNS object
WiFiUDP           UDP;                                          // UDP protocol on STA interface, localPort
ESP8266WebServer  webServer(80);                                // HTTP web server on common port 80

boolean connectUDP()                                            // connect to UDP – returns true if successful or false if not
{
  boolean state = false;
  Serial << endl << (F("Connecting to UDP ===> ")) ;

  if (UDP.begin(localPort) == 1) {
    Serial << (F("Connection successful")) ;
    state = true;
  }
  else {
    Serial << (F("Connection failed")) ;
    delay(50);
  }
  return state;
}

void Listeners() {
  webServer.handleClient();
  delay(0);                                                   // allow RTOS to breath

  int packetSize = UDP.parsePacket();                         // if there’s data available, read a packet
  if (packetSize)
  {
    Serial << endl;
    Serial << (F("Received packet of size "));
    Serial << (packetSize);
    Serial << (F("From "));
    IPAddress remote = UDP.remoteIP();

    for (int k = 0; k < 4; k++)
    {
      Serial << (remote[k], DEC);
      if (k < 3)
      {
        Serial << (F("."));
      }
    }
    yield();
    Serial << (F(", port "));
    Serial << "UDPremotePort" << (UDP.remotePort()) << endl;

    UDP.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE);           // read the packet into packetBufffer
    Serial << (F("Contents: "));
    int value = packetBuffer[0] * 256 + packetBuffer[1];
    Serial << (value) << endl;
    ++ack_Count;                                              // added mrb to give an ack serial-number
    Serial << (F("Ack counter: "));
    Serial << (ack_Count);                                    // ever increasing serialnumber
    intToStr(ack_Count, ReplyBuffer, 12);                     // 12 characters wide
    yield();
    UDP.beginPacket(UDP.remoteIP(), UDP.remotePort());        // send a reply, to the IP address and port that sent us the packet we received
    UDP.write(ReplyBuffer);
    UDP.endPacket();

    digitalWrite(2, value);                                   // turn LED on or off depending on value received
  }                                                           // if(packetSize)
}

void GPSstuff(char c) {                                       // GPSbuffer[] is global
  static int i, j;                                            // persistent within function scope
  static char q;
  static bool flag = false;
  static char GPSbuffer[120];                                  // GPS serial line input buffer
  q = c;

  if ( q == 0x24 )                                             // '$'
  {
    i = 0;                                                     // brute force sync on '$' to GPSbuffer[0]
    // Serial << "Found $" << endl;
  }

  if ( i < 120) GPSbuffer[i++] = q;
  // Serial << "Index=" << (i -1) << "Input=" << q << endl;
  //if (i = 120) i = 119;                                      // array limit safety

  if (q == 0x0d) {
    flag = true;                                               // is the character a CR for eol
    i = 0;
  }

  if (flag) {                                                  // test for end of line and if the right GPSbuffer
    flag = false;                                              // reset for next time
    // Serial << "We are in the flag routine..." << GPSbuffer[3] << GPSbuffer[4] << GPSbuffer[5] << endl;
    // Serial << "Analyzing " << GPSbuffer[3] << GPSbuffer[4] << GPSbuffer[5] << endl;
    if ( (GPSbuffer[3] == 0x52) && (GPSbuffer[4] == 0x4d) && (GPSbuffer[5] == 0x43)) // 'R' && 'M' && 'C'
    {
      for (j = 0; j < 120 ; j++) {
        UDP.write(GPSbuffer[j]);
      }
      UDP.write("\r\n");                                         // terminate the line
      UDP.endPacket();                                           // clear UDP buffer
    }
  }
}


void setup()
{
  Serial.begin(9600);                                           // Initialise Serial connection
  Serial << (F("(c) 2015 M. Ray Burnette")) << endl;
  Serial << (F("Tardis Time Portal 0.20150915")) << endl << endl;

  WiFi.mode(WIFI_AP_STA);                                       // AP + STA
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));   // subnet FF FF FF 00
  WiFi.softAP("TardisTime");

  dnsServer.start(DNS_PORT, "*", apIP);                       // "*" creates a captive portal

  webServer.onNotFound([]() {                                 // replay to all requests with same HTML
    webServer.send(200, "text/html", responseHTML);
  });

  webServer.begin();                                          // Start HTTP services

  while (! connectUDP() ) {                                   // UDP protocol connected to local
    Serial << "+" ;
    yield();                                                  // Play nicely with RTOS (alias = delay(0))
  }

  // This will loop forever if UDP fails
  Serial << endl;
  Serial << (F("Setting pin#2 to Output mode")) << endl;
  pinMode(2, OUTPUT);                                         // initialise pin mode
}


void loop()
{
  dnsServer.processNextRequest();                             // TCP Address handler when requested
  delay(0);                                                   // yield for RTOS
  Listeners();                                                // UPD and HTTP
  delay(0);
  if (Serial.available() > 0) {                               // anything in the serial hw buffer?
      char c = Serial.read();                                 // if so, fetch the next character from FIFO
      GPSstuff(c);
  }
  delay(0);
}                                                             // loop repeats forever unless stack crashes or uC hangs


// Written by Dave St. Aubin ---> see Notes.h
#ifndef UTILITYFUNCTIONS_H
#define UTILITYFUNCTIONS_H

float powr(float x, int y);
void reverse(char *str, int len);
int intToStr(int x, char str[], int d);
void ftoa(float n, char *res, int afterpoint);

#endif //  UTILITYFUNCTIONS_H

// Written by Dave St. Aubin ----> see Notes.h
#include "Utility.h"

/*******************************************************
 * Replaces the math.h pwr function since we cannot
 * successfully link to it with the ESP8266 Arduino IDE
 *******************************************************/
float powr(float x, int y)
{
    float temp;
    if( y == 0)
       return 1;
    temp = powr(x, y/2);
    if (y%2 == 0)
        return temp*temp;
    else
    {
        if(y > 0)
            return x*temp*temp;
        else
            return (temp*temp)/x;
    }
}

// reverses a string 'str' of length 'len'
void reverse(char *str, int len)
{
    int i=0, j=len-1, temp;
    while (i<j)
    {
        temp = str[i];
        str[i] = str[j];
        str[j] = temp;
        i++; j--;
    }
}

 // Converts a given integer x to string str[].  d is the number
 // of digits required in output. If d is more than the number
 // of digits in x, then 0s are added at the beginning.
int intToStr(int x, char str[], int d)
{
    int i = 0;
    while (x)
    {
        str[i++] = (x%10) + '0';
        x = x/10;
    }

    // If number of digits required is more, then
    // add 0s at the beginning
    while (i < d)
        str[i++] = '0';

    reverse(str, i);
    str[i] = '\0';
    return i;
}

// Converts a floating point number to string.
void ftoa(float n, char *res, int afterpoint)
{
    // Extract integer part
    int ipart = (int)n;

    // Extract floating part
    float fpart = n - (float)ipart;

    // convert integer part to string
    int i = intToStr(ipart, res, 0);

    // check for display option after point
    if (afterpoint != 0)
    {
        res[i] = '.';  // add dot

        // Get the value of fraction part upto given no.
        // of points after dot. The third parameter is needed
        // to handle cases like 233.007
        fpart = fpart * powr(10, afterpoint);

        intToStr((int)fpart, res + i + 1, afterpoint);
    }
}

Do remember that the time a GPS puts out may not be UTC, even if it reports a fix, but can be out a couple of seconds.

Interesting stuff. NTP wasn't also a valid option ?

I already have one satellite clock using an obsolete AI Thinker A7 module and a Bluepill but based loosely on an "Elecrow" ATmega32U4 design. There I use the time delivered by the module except at startup where I use the Bluepill's inbuilt RTC if that has a plausible time.

That is what I up to now with a much simpler to use GPS module and a much less featured MCU and am revisiting some of the design decisions I made earlier.

Next to the satellite clock I have a Nixie NTP clock. It is very satisfying to see the seconds of both clocks click over simultaneously. My ambition is to have a 6 digit DCF77 clock there as well.

A couple of seconds sounds quite grim. I'm hoping this is an extreme edge case.

Quite normal actually.

Most all GPS will not initially have the seconds correct.

Most of mine can be one second out, I have a couple (Neo6s) that are two seconds out.

A GPS can only be sure to be putting out UTC when it has received the leap seconds update navigation message that goes out every 12.5 minutes and with no absolute guarantee its received of course.

Leap seconds don't occur all that often and most GPS modules have battery backed memory or EEPROM for configuration information. Are you talking about a specific clean start case or have I misunderstood something ?

Thats the clean start situation.

The GPS network time is not UTC. Its the time in early 1980.

A GPS then displays a time which is 1980 time plus the number of leap seconds the GPS has programmed into its firmware, which in almost all cases will not be the current number of leap seconds. Hence the need for the leap seconds update message.

Clearly, to rely on the time the GPS puts out, you need to be checking the status of the leap seconds update, as in whether the GPS has had it.

At the time I was writing an article on ESP8266 and UDP.

One of these days I may repost those old projects; I am no longer associated with the site I used in the past.

Ray

6v6gt,
I've got 2 clocks that use GPS to get the time.

They have both been running for months, however one of them recently had the USB lead accidentally disconnected.

When I reconnected it, a few hours later the display initially said 00:00:00 for a few seconds, it soon changed to indicate a time that was 2 seconds wrong. After approximately 1 minute it was displaying the correct time.

When I disconnected it for one minute, just to see what happens, it came back on the correct time almost immediately.

I'm not looking at the full NMEA sentences, just parsing the required time data, so I cant tell you exactly how many satellites were being received at any time.
Hope this gives you some useful information.

OK. Thanks. I guess what happened there is that the current value of leap seconds was in battery backed memory. When the power was disconnected for a protracted period, the memory was lost. After a restart and wait (a) the battery was charged and (b) a new value for leap seconds was received.

Quite so.

So to be sure to avoid puplishing the wrong time standard the status of the leap seconds update in the GPS has to be confirmed.

Up until 2016 a leap second was added about once every 2 years, but there has now not been one for the last 5 years so an addition might be due soon. Maybe you need to detect and deal with the situation when the number of leap seconds changes from the current value of 19.

When there were only 2 GPS satellites in orbit. I was tasked with setting up a ground based Atomic clock that simulated the necessary 3rd satellite. What I remember was that I had to fool the calculations into 'thinking' that the atomic clock was in space and that I had to compensate that GPS satellites told where they were at not where they are now. My GPS unit serial numbers were A and B from Motorola and were the size of toaster ovens.

Thanks for the memories.