A VERY basic Arduino GPS Based Timing - For an car race

Hi Guys,
Hoping to get some input into my little GPS timing project.

What i a trying to do:
Have two GPS modules each with their own Arduino Nano and wireless module (nrf24l01+pa+lna) in two seperate locations. One for start One for finish. Almost directly inbetween the start and finish i am planning on having a Midpoint station as well (this is where i want the start and finish times to be sent via the wireless). I am using the GPS pulse (pulses "exactly" every second) to measure millis.
In determining each car's time i am recording the time when the beam gets broken and then sending this via wireless serial to the Midpoint. By doing this at the finish i can then work out the time it took the car to complete the course using the GPS system as my common clock source. If i duplicate the hardware and software then whatever the delay is in the start will also be in the end so they they will cancel out (within a millisecond or so ?)

I built the first prototype everything appears to be working, then i started testing.

I used a second arduino and basically used the blink file to as a trigger. I was hoping to get very consistent results however my standard deviation was 18.2 milliseconds for my on period and 17.9 millisecond for the off period.

I was really hoping to get to a 1millisecond standard deviation.

Please find the code i used for testing below.

I was unable to include the serial outputs well as the analytics on the serial output so i have included them in the txt files.
"Timing Serial output" is just the output from the Start Arduino.
"On - OFF timing differences" is the calculated differences between the on and the off, i then also took the STD of the on times and the off times which really surprised me and cannot understand where this large deviation is coming from.

The blink code i used was
Loop
{
digitalWrite(LED,HIGH);
delay(33333);
digitalWrite(LED,LOW);
delay(11111);
}

Again i just wanted to test the consistency.

Would really appreciate any advice and guidance on this project.

Sorry for any bad coding, i have limited coding experience.

Thanks for your time and i look forward to your response.

Chris

#include <SoftwareSerial.h>
#include <TinyGPS++.h>

SoftwareSerial GPSSerial(5, 4);
TinyGPSPlus gps;

double UTC_Offset = 4;
const int Snubber_delay = 3; // in seconds
volatile int SatNum;
volatile bool GPS_Waiting = false; // Indicates that we are still waiting
volatile bool GPS_Ready = false; // Bool that holds whether the GPS has an non zero hour value

volatile int ClockHour;
volatile int ClockMinute;
volatile int ClockSecond;

volatile int SnubClockHour;
volatile int SnubClockMinute;
volatile int SnubClockSecond;

volatile int GPSClockHour;
volatile int GPSClockMinute;
volatile int GPSClockSecond;
volatile bool new_Time = false; // GPS clock updated flag
volatile bool Laser_Monitoring = true; // Laser tripped Snubber

unsigned long Trigger_Pulse = 0; // Holds first the millisecond time elapsed from the last GPS clock pulse to the laser trigger time.
unsigned long GPS_Pulse_ms_Timer = 0; // millis() value of last GPS clock pulse

const int GPS_Clock_Pulse_Pin = 2; //ISR pin from opamp output
const int TriggerPin = 3; //ISR pin from opamp output
const int pwmPin = 6;      // voltage regulator for op-amp
const int LaserPin = 7;      // voltage regulator for op-amp
int sensorPin = A0;
int VoltageRead = 0;

void setup()
{

  //pinMode(LaserPin, OUTPUT);// Laser Trigger ISR
  //digitalWrite(LaserPin,HIGH); // for testing

  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  // set the data rate for the SoftwareSerial port
  GPSSerial.begin(9600);
  delay(1000);
  Serial.println("Starting");
  pinMode(TriggerPin, INPUT);// Laser Trigger ISR
  attachInterrupt(digitalPinToInterrupt(TriggerPin), SubCaptured, RISING); // Laser Trigger ISR

  pinMode(GPS_Clock_Pulse_Pin, INPUT);
  attachInterrupt(digitalPinToInterrupt(GPS_Clock_Pulse_Pin), SubSecondTimer, RISING); // GPS every second rising edge interrupt
}

void SubCaptured() // Laser Trigger ISR
{
  if (Laser_Monitoring) // ISR can only be every (Snubber_delay = 3) Seconds
  {
    Trigger_Pulse = millis() - GPS_Pulse_ms_Timer; // calculate Time elapsed since last GPS Clock Pulse
    if (Trigger_Pulse > 999)
    {
      SnubClockSecond = ClockSecond + (int)(Trigger_Pulse / 1000);
      SnubClockMinute = ClockMinute;
      SnubClockHour = ClockHour;
      Laser_Monitoring = false;
    }
    else
    {
      SnubClockHour = ClockHour;
      SnubClockMinute = ClockMinute;
      SnubClockSecond = ClockSecond;
      Laser_Monitoring = false;
    }
    Serial.println("Snap shot: " + String(SnubClockHour) + ":" + String(SnubClockMinute) + ":" + String(SnubClockSecond) + ":" + String(Trigger_Pulse));
  }

}

void SubSecondTimer() // occurs every time the GPS Pulses (every second)
{
  GPS_Pulse_ms_Timer = millis(); // 'reset' ms Timer

  if (ClockSecond < 60)
  {
    ClockSecond = ClockSecond + 1;
  }
  else
  {
    ClockSecond = 0;
    if (ClockMinute < 60)
    {
      ClockMinute = ClockMinute + 1;
    }
    else
    {
      ClockMinute = 0;
      if (ClockHour < 24)
      {
        ClockHour = ClockHour + 1;
      }
      else
      {
        ClockHour = 0;
      }
    }
  }
  if (ClockHour == 0 )
  {
    if (ClockHour == 0 & !GPS_Waiting)
    {
      Serial.println("Waiting for GPS Clock");
    }
    GPS_Waiting = true;
  }
  else if ( ClockHour != 0 && !GPS_Ready)
  {
    GPS_Ready = true;
    Serial.println("GPS Ready");
  }
  //Serial.println("Local_Time: " + String(ClockHour) + ":" + String(ClockMinute) + ":" + String(ClockSecond) + " -- Sat #:" + String(SatNum));
  //Serial.println("Monitoring And we had a GPS Clock Pulse");
}

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

  float VoltageRead =  analogRead(sensorPin) * (4.73 / 1023.0); // should be 1023 but calibrated it for resistances
  analogWrite(pwmPin, (VoltageRead - 0.025) * 52); //

  while (GPSSerial.available() > 0)
  {
    if (gps.encode(GPSSerial.read()))
    {
      GPS_Update();
      if (new_Time)
      {
        ClockHour = GPSClockHour;
        ClockMinute = GPSClockMinute;
        ClockSecond = GPSClockSecond;
      }
      //Serial.println("Local_Time: " + String(ClockHour) + ":" + String(ClockMinute) + ":" + String(ClockSecond));
      //Serial.println("");
    }

  }
  if ((ClockHour * 60 * 60 + ClockMinute * 60 + ClockSecond) > (SnubClockHour * 60 * 60 + SnubClockMinute * 60 + SnubClockSecond + Snubber_delay ) )
  {
    Laser_Monitoring = true;
  }

}

void GPS_Update()
{

  if (gps.time.isValid())
  {

    if (gps.time.hour() != GPSClockHour && gps.time.minute() != GPSClockMinute && gps.time.second() != GPSClockSecond)
    {
      new_Time = true;
    }
    else
    {
      new_Time = false;
    }

    //New GPS Clock Update

    if (gps.time.hour() < UTC_Offset) // UTC_Offset = 4
    {
      //Serial.print(String(24 - UTC_Offset + gps.time.hour())); //
      GPSClockHour = 20 + gps.time.hour();
    }
    else
    {
      //Serial.print(gps.time.hour() - UTC_Offset);
      GPSClockHour = gps.time.hour() - UTC_Offset;
    }
    //Serial.print(":");

    if (gps.time.hour() < 4)
      GPSClockMinute = gps.time.minute();
    //Serial.print(gps.time.minute());
    //Serial.print(":");

    if (gps.time.hour() < 4)
      //Serial.println(gps.time.second());
      GPSClockSecond = gps.time.second();

  }
  else
  {
    Serial.println("Invalid Clock Data");
  }
  SatNum = gps.satellites.value();
  //Serial.print("Sat #:");
  //Serial.println(gps.satellites.value());
}

Timing serial output.txt (2.81 KB)

On - OFF timing differences.txt (855 Bytes)

How far apart are they? Have you tested the range?
I have never worked with the NRF24L01 before, but I have added it to my to-do list.

Why are you using GPS? If all you need it to time the car from point A to point B, just use the transceiver on Arduino 1 to tell Arduino 2 that the car has started. Then count millis until the car breaks the beam at Arduino 2.

Hi Stevemann,
Thanks for the reply,

Nope i have not tested the wireless aspect yet, i am just testing the GPS timing and tripping aspects of the project.

The race is actually linear going over a ridge with lots of trees, so i may have to use 1 or two repeaters which is why i wanted to avoid using two separate clocks sources and decided to go GPS.

Side note the NRF24L01 are really easy to use and have good error checking (if you get the message chances are - it is accurate).

However with error checking you never know if it was the first transmission attempt or the second etc that was successful, therefore if it needs to resend the transmission then your timing is going to be off depending on which transmission was successfully received. That was why i wanted to use GPS so that which transmission (1,2,3,4 etc) is successful becomes irrelevant as the message contains the time.
Again that was my train of thought on not doing it the way you suggested, would welcome any suggestions if i am wrong on this.

Hope i answered all your questions.

Lots of problems with the posted code.

First, never print from within an interrupt routine, as printing depends on interrupts, which are turned off in an interrupt routine.

Second, avoid use of Strings, they cause memory problems and program crashes on an AVR based Arduino.

Third, global variables modified by an interrupt routine must be protected against corruption by the interrupt, while being accessed by the main program. Make copies of shared variables with the interrupts off.

Example:

        noInterrupts();
        ClockHour = GPSClockHour;
        ClockMinute = GPSClockMinute;
        ClockSecond = GPSClockSecond;
        interrupts();

However, if you want race timing to be more accurate than +/- 1 second, I don't think your approach to use the time obtained from GPS module will work, because of the latency of the GPS receiver and serial communications.

Instead, I suggest to have the different Arduinos keep time independently, using the GPS 1PPS interrupt and micros() for the fraction, synchronized at startup to the second by the GPS system.

The PPS output from the GPS should have an accurate edge, although the timing of that will vary slightly as the location changes.

However because of the leap seconds issue the GPS time from seperate GPS can vary by a number of seconds, until they receive the leap seconds update correctly which is sent out every 12.5 minutes.

Hi srnet,
Thanks for your reply.
So if i understand you correctly, if i have two GPS modules and compare the PPS of both they would each be spaced out exactly the same (1 second) however the pulses would not necessarily be firing at the same time.
Do you know how much the PPS will vary by location. I would be happy with <2-3 ms variation, ideally <1ms. My GPS modules will be within a mile of each other.

By leap second i assume you mean how the GPS does not always update every second... My plan around this was to only use the GPS time simply to synchronize the two clocks so each GPS update would synchronize my two separated GPS clocks (apparently every 12.5 minutes?) updating hours, minutes and seconds and while i wait for the GPS update I use the PPS to increment my seconds locally.

But like you said since the PPS are not synchronized this strategy might not work.

Hope i understood your comments correctly

Thanks

Hi jremington,
Sorry i originally missed your comment.

I assumed that you cannot print in an ISR , but i saw someone else doing it and though i must have been wrong... thats what you get for believing everything on the internet is good...

With regards to your second point, using strings, i assume you mean something like this
Serial.println("Snap shot: " + String(SnubClockHour) + ":" + String(SnubClockMinute) + ":" + String(SnubClockSecond) + ":" + String(Trigger_Pulse));

Could you give me an example on this as well, should i use Char's string?

Third point - understood - makes sense, thanks

Ultimately yes i want the system to be accurate to 1ms, so it seems i need to abandon my GPS modules and simply use one for startup synchronization get the clocks all the same and then deploy arduino's to their respective Race locations.
I found on other Arduino forums that the clocks can vary by 50PPM. If thats the case then 'worst case scenario' 50/16M would vary by 0.0003125% ? Going further if my race time was 30 seconds my error would be 93 micros, after 1 hour of racing i could expect (worst case) an error of 11.25ms, after 12 hrs 135ms... hmmm hoping i made a mistake because i need better performance. Last year some of the races came down to a few ms...

I guess i could correct my clocks if i know what the offset assuming it is constant?

THanks for your input, and the advice about the coding. Greatly appreciated

Synchronization of GPS-based timing is really tricky, with lots of pitfalls. I did not know about the leap second problem, so thanks srnet! But, the rising edge of the 1PPS output is supposedly good to a few nanoseconds.

Two ways to print time using Arduino, avoiding Strings:

  1. Use the tedious way, with "Arduino speak"
Serial.print(hours);
Serial.print(":");
Serial.println(minutes); // etc.
  1. Short, fast and most flexible way, with zero-filled numerals using snprintf()
char buf[30];
snprintf(buf,sizeof[buf],"Snap shot %02d:%02d:%02d",hours,minutes,sec);  //ints assumed
Serial.println(buf);

Hi Jremington,

Thanks for the example, exactly what i needed.

With regards to the nano's clock do you know if the error is constant. I know the clocks will vary with temperature however assuming the environment will be the same at both locations (1 mile apart), the error should be constant enough to make corrections?

Thanks

You will have to experiment to determine the clock error and the temperature dependence thereof. It depends on the crystal or resonator selected during manufacture.

Here is a program that will use the GPS 1PPS signal on pin D8 as a time base to measure the actual Nano clock period (in clock cycles per second).

// Frequency timer using input capture unit
// Author: Nick Gammon
// Date: 31 August 2013
// modified to just count the crystal clock, added 10 period averaging JR 2015
// Input: Capture signal on Pin D8

volatile boolean first;
volatile boolean triggered;
volatile unsigned int overflowCount;
volatile unsigned long startTime;
volatile unsigned long finishTime;

// timer overflows (every 65536 counts)
ISR (TIMER1_OVF_vect)
{
  overflowCount++;
}  // end of TIMER1_OVF_vect

ISR (TIMER1_CAPT_vect)
{
  // grab counter value before it changes any more
  unsigned int timer1CounterValue;
  timer1CounterValue = ICR1;  // see datasheet, page 117 (accessing 16-bit registers)
  unsigned long overflowCopy = overflowCount;

  // if just missed an overflow
  if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 0x7FFF)
    overflowCopy++;

  // wait until we noticed last one
  if (triggered)
    return;

  if (first)
  {
    startTime = (overflowCopy << 16) + timer1CounterValue;
    first = false;
    return;
  }

  finishTime = (overflowCopy << 16) + timer1CounterValue;
  triggered = true;
  TIMSK1 = 0;    // no more interrupts for now
}  // end of TIMER1_CAPT_vect

void prepareForInterrupts ()
{
  noInterrupts ();  // protected code
  first = true;
  triggered = false;  // re-arm for next time
  // reset Timer 1
  TCCR1A = 0;
  TCCR1B = 0;

  TIFR1 = bit (ICF1) | bit (TOV1);  // clear flags so we don't get a bogus interrupt
  TCNT1 = 0;          // Counter to zero
  overflowCount = 0;  // Therefore no overflows yet

  // Timer 1 - counts clock pulses
  TIMSK1 = bit (TOIE1) | bit (ICIE1);   // interrupt on Timer 1 overflow and input capture
  // start Timer 1, no prescaler
  TCCR1B =  bit (CS10) | bit (ICES1);  // plus Input Capture Edge Select (rising on D8)
  interrupts ();
}  // end of prepareForInterrupts


void setup ()
{
  Serial.begin(115200);
  Serial.println("Frequency Counter");

  pinMode(8, INPUT_PULLUP);
  pinMode(7, OUTPUT);
  digitalWrite(7, LOW);
  pinMode(13, OUTPUT);
  digitalWrite(13, HIGH);


  // set up for interrupts
  prepareForInterrupts ();
} // end of setup

void loop ()
{
  static unsigned long average = 0;
  static int n = 0;
  // wait till we have a reading
  if (!triggered)
    return;

  PINB |= (1 << 5); //blink LED

  // period is elapsed time
  unsigned long elapsedTime = finishTime - startTime;
  // frequency is inverse of period, adjusted for clock period
  //  float freq = F_CPU / float (elapsedTime);  // each tick is 62.5 nS at 16 MHz

  Serial.println (elapsedTime);
  average += elapsedTime;
  n++;
  if (n == 10) {
    Serial.print("Average of ten: ");
    Serial.println(average / 10);
    n = 0;
    average = 0;
  }
  //  Serial.print ("Frequency: ");
  //  Serial.print (freq);
  //  Serial.println (" Hz. ");

  // so we can read it
  delay (500);

  prepareForInterrupts ();
}   // end of loop

Wow thanks so much for the code!!!
I ran the code with PPS connected to D8 and after 1500 data points i summarized the following

Nano Results:
At 1500 Data points we get
STD: 49.69705
Min: 16008323
Avg: 16008405
MAX: 16008534

Range (Max- Min): 211
Max - Avg: 128.6147
AVG - Min: 82.38533

With Uno Results:

At 1500 Data points we get
STD: 115.3133
Min: 15988374
Avg: 15988451
MAX: 15988978

Range (Max- Min): 604
Max - Avg: 527.282
AVG - Min: 76.718

Amazing how much it varies, i always though it was more stable.
Thanks again for the guidance

vermin_sapper:
I assumed that you cannot print in an ISR , but i saw someone else doing it and though i must have been wrong... thats what you get for believing everything on the internet is good...

In the old Arduino days printing from an ISR reportedly simply didn't work. This has been changed, printing from an ISR works but it's still a bad idea. An ISR should be as short as possible, if there's more work to do (like printing) set a flag for the main loop() to pick up and do the hard work.

vermin_sapper:
Wow thanks so much for the code!!!
I ran the code with PPS connected to D8 and after 1500 data points i summarized the following

[...]

Range (Max- Min): 211

[...]

Range (Max- Min): 604

The first number is 13 ppm variation. The second number 38 ppm. That's indeed pretty amazing, so low numbers, especially the Nano which normally uses a ceramic resonator - 13 ppm is really amazing. It means you're off by 13 us per second, and as race timings are normally to a precision of 0.01 seconds (there's not much hope to get anything better than that using consumer grade equipment) this is definitely good enough, three orders of magnitude lower than your measurement resolution.

Do realise that your break beam sensors also have reaction time, which can very well be much larger than this variation...

vermin_sapper:
Hi srnet,
Thanks for your reply.
So if i understand you correctly, if i have two GPS modules and compare the PPS of both they would each be spaced out exactly the same (1 second) however the pulses would not necessarily be firing at the same time.
Do you know how much the PPS will vary by location.

The rising edge of PPS should be in sync across GPSs to a few 10s of nS. Its can vary dending on the GPSs position error, but again only in the 10s of nS.

GPSs have a value for leap seconds in firmware, so this may or may not be the current value, and a particular GPS may or may not have received the update from the satellites.

Check the GPSs datasheet, there will be a flag somewhere telling you if the leap seconds has been updated.

Hi mvmarle,srnet

mvmarle,
Yes i have phototransistors for my light sensors. I cannot exactly remember exactly what the delay is but the delay is WELL below mS range. And is also relatively constant so i can work backwards to compensate for the delay.
With regards to the measurement resolution "and as race timings are normally to a precision of 0.01 seconds" well... we had quite a few very close calls last year where their time differences was 0.003. This was done with a wired system. Last year was my first time helping out on the course with ham radios so maybe it was just a very close year.?. The course is short so all the cars come in around the 'same time'.
What amazed me so much is that the clocks were so different, just looking at averages at 16Mhz they were different by almost 20kHz. For some reason i just always thought clocks would vary around 1kHz. But thanks again for jremington for the code was very interesting and a great learning experience

srnet,
I think i should do a test and determine the accuracy/consistency of the PPS of two GPS modules.
But hypothetically if the difference is 10nS then after 24 hrs (86400 seconds) i could expect a deviation of 0.864 mS.
Other than using a GHZ oscilloscope (that i do not have) how do i measure the time difference between two GPS PPS signals? If my math is right, its just over a tenth of one CPU clock pulse at 16Mhz...

Ideas on testing?

vermin_sapper:
But hypothetically if the difference is 10nS then after 24 hrs (86400 seconds) i could expect a deviation of 0.864 mS.

The difference between the time signals of those satellites is way less than 10 ns when the signals are transmitted. That 10 ns is due to time of flight difference, and is what the whole location calculation is based upon.
These satellites not only are constantly synchronising themselves with ground stations running atomic clocks, they have three atomic clocks each on board, which are corrected for relativistic effects like the speed of the satellites themselves.
Now translating that kind of accuracy onto an Arduino is not that easy.
If you really want the clock to be more precise, you have to find a board with a crystal rather than resonator. That 1 kHz/16 MHz is 62.5 ppm, a resonator is typically within 200-300 ppm, a crystal easily does 50 ppm, better crystals can get down to 10-20 ppm.
Getting the time, and when exactly the second rolls over, from the GPS is a whole different story as discussed above. An interrupt will help, you will also need to get a GPS receiver that has the ability to actually send out that seconds pulse with sub-microsecond precision.

vermin_sapper:
srnet,
I think i should do a test and determine the accuracy/consistency of the PPS of two GPS modules.
But hypothetically if the difference is 10nS then after 24 hrs (86400 seconds) i could expect a deviation of 0.864 mS.

Its not a cumulative variation, after 24 hours it will still be within 10nS or so of real time.

If the time crept like that you would expect the GPSs position to slowly move and dissapear over the horizon at the rate of 259km a day .........

Perhaps do some research, a Google search on 'GPS Clock Synchronisation' for instance ?

Anyway I thought you said this was 'VERY basic Arduino GPS Based Timing'

Hi Guys,
sorry for the delay work got in in the way of play :slight_smile:

wvmarle,
I am thinking i should use the GPS PPS as my clock source for my start point using my ISR to trigger an update to my timer. Will take some rewriting but i believe i need to redo most of my code (remove everything from my ISR except what is needed etc). Good learning opportunity.
Then basically duplicate this at the finish line. When it gets time for race day then i will sync my clocks in the morning and i should be accurate to < mS.

I will do tests along the way to ensure that my clock accuracy is good.

I have done a millis count between my PPS signal and they are basically 1000, so it seems this is th way i need to go.

srnet,
yeah for some reason my projects always start sounding very basic... just need to be accurate to 1ms... how hard can it be my clock is 16MHZ... famous last words. :slight_smile:
That makes sense, if it was cumulative then things would be moving + like you said the satellites are constantly getting synchronized.

Will dig in Google in GPS Clock Synchronization

I will update here as i go along.

Thanks again to everyone for the guidance. I have learned a lot already :slight_smile:

vermin_sapper:
Hi Guys,
sorry for the delay work got in in the way of play :slight_smile:

Isn't it supposed to be the other way around??

vermin_sapper:
... just need to be accurate to 1ms... how hard can it be my clock is 16MHZ... famous last words. :slight_smile:

Things that may help avoid creeping latency.
Don't use delay() statements in your loop. Use millis() or micros() for timing.
Don't use any String class function. I haven't measured it because I have no projects that are that time critical, but I have been told that String operators take a bit longer to execute than strings. I stopped using String class because it uses a lot of Ram memory.

Will do,
Thanks for the tips Stevemann

p.s. its relative, depends on what your priorities are in life. I have the rest of my life to work, playtime is limited :wink:

Hope you have a great day