IntervalTimer inaccuracy? Discipline? Scope Video included

Status
Not open for further replies.

Talkiet

Well-known member
I'm trying to precisely set a clock from a cheap GPS unit with PPS output on a Teensy 3.5. I have the code apparently working and in manual comparisons (pressing a button while watching a clock) to a real time source it's very good and clearly is using the PPS appropriately.

I am using the elapsedmillis() function to graft on milliseconds to the time by taking a baseline value immediately after setting the clock (which in turn happens immediately after the PPS pulse starts)

However as part of proper testing I have an Intervaltimer configured to pulse an output pin on 0 milliseconds (actually on 1000000 microseconds basis - heartbeattime=1000000)

Code:
  elapsedMillis secondcounter, msoffset;

[snip]

  msoffset = 0;
  testtrigger.begin(pulseout, heartbeattime);

[snip]

  void pulseout() {
  digitalWrite(ppsout,HIGH);
  ms=(msoffset % 1000);
  digitalClockDisplay(local);
  testtriggeroff.begin(pulseoutoff, 100);
}

I have a put both the PPS on the GPS board and the digital out pin on the Teensy3.5 into a scope and while I would happily accept there could be delays in my code meaning I might observe a static offset of the PPS high and the Teensy pin high, I didn't expect to see the Teeny wander away at the rate it is.

If a picture is worth a thousand words, then a video must be worth heaps more.


Sorry for the lack of full code. It's ugly++ at the moment as I am just figuring out a precise time setting mechanism - however given the video above seems to show a significant drift in the IntervalTimer, that's my specific question - is that expected? Is there a way to discipline the timer used for that?

For reference I did run the code here ( https://forum.pjrc.com/threads/24563-Question-re-RTC-compensation-function ) and got about -21ppm equating to [Teensy3Clock.compensate(181);] but that doesn't seem to work. I note that thread is from 2013 and maybe it's not relevant anymore.

Cheers - Neil G
 
Running against GPS PPS here the Teensy clocks ( T_3.6 and 4.0 ) typically finish get fewer cycles completed per GPS second that 'clock speed'. So that will be one thing to track and offset where the PPS is a true second.

Timers and anything millis and micros based will have that error cooked in.

Just turned on a pair of T_4.0's - the first to GPS lock is seeing '599996938' cycles between PPS. After a minute the LOW and HIGH vary by 324 cycles >> [599,996,707 - 599,997,031]. Those are in a cooler place in the window and have been off some weeks and just warming up - and the second GPS isn't locked yet after some minutes.

With snippet of code not giving a real idea of what is going on it is hard to say if there are other errors in measurement.

Is there an interrupt triggering on the PPS Rising? Like : attachInterrupt(gpsPin, gpsISR_PPS, RISING); // set flag high on rising edge

If that is so and has code the CYCCNT can be enabled on a T_3.5 the diff between readings will show how many clock cycles happen between those PPS events. Here a CYCCNT enable:
Code:
void setup() {
  if ( ARM_DWT_CYCCNT == ARM_DWT_CYCCNT ) {
    ARM_DEMCR |= ARM_DEMCR_TRCENA;  // info from teensy forum, access cycle counter
    ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;
  }

Here is the code tracking the PPS CYCCNT observed and the loop has plenty of chance to monitor that gpsEdge for change against the last recorded change.:
Code:
volatile uint32_t gpsEdge = 0;

void gpsISR_PPS() {
  gpsEdge = ARM_DWT_CYCCNT;
}

Those snippets are from a thread on a 'Pendulum' question with complete code if it helps.
 
To answer your 2 questions...


the video above seems to show a significant drift in the IntervalTimer, that's my specific question - is that expected?

No. This much drift is not normal. Timing is based on the 24 MHz crystal, which should error less than 30ppm.


Is there a way to discipline the timer used for that?

Directly in hardware, no. With carefully crafted software, maybe, probably.

But adding even more complexity to this code described as "ugly++ at the moment" and too much to share here on the forum would really be putting the cart before the horse. You should first create a much simpler program to investigate only this issue.

I'd recommend starting fresh with code using repetitive calls to digitalReadFast() to detect the rising edge of the 1pps pulse. The moment you see a low then a high, start an IntervalTimer with 1000000 us period, and have it run that function which just pulses another pin. Do this only once in setup(), and put no code at all in loop(). Just let the IntervalTimer keep running the pulse function every second.

Then watch the 2 signals on your oscilloscope. Obviously there will be some lag from the rising edge to when IntervalTimer runs the pulse function, but it should not drift much.

That code should be very simple and something you can easily post here on the forum....
 
Ok, I tried it here, and indeed I'm seeing the problem with this simple program. Maybe we have a bug in IntervalTimer?

Code:
IntervalTimer mytimer;

void setup() {
  pinMode(2, INPUT);
  pinMode(4, OUTPUT);
  while (digitalReadFast(2) == HIGH);
  while (digitalReadFast(2) == LOW);
  mytimer.begin(pulse, 1000000.0 - 14.2);
}

void pulse() {
  digitalWriteFast(4, HIGH);
  delayMicroseconds(1);
  digitalWriteFast(4, LOW);
  //static unsigned int count=0;
  //Serial.printf("Pulse %u\n", count++);
}

void loop() {
}
 
Chuckle... I had opened a new sketch and was starting to write what you described when I thought "I wonder if that was the last reply" and then read your example code :)

To answer defragster.. the interrupt is configured as follows:

Code:
  pinMode(beamint, INPUT);
  attachInterrupt(beamint, isrService, RISING); // interrrupt on light beam input

At the moment the code supports resyncing to the GPS PPS pulse every [confiugurable] seconds so although it is hideously inelegant, I can maintain enough accuracy just be having it resync every 30 seconds or so - but that makes my skin crawl if I am honest :)

But also even in the quick replies so far I have learnt about the existence of digitalReadFast (as opposed to the digitalRead I am using now)... I don't think it'll make any real difference to my use case but learning new things is always good :)

Here's a video of the exact same hardware setup running the code you supplied Paul... Also, why are you subtracting 14.2 from the intervaltimer micros metric?


Cheers - N
 
Although I presume it will vary with temperature, my T3.5 with stock frequency needed that to be 20.1 at 21C ambient.

Actually, yes it is varying with temp... Blowing on the T3.5 I can change the speed and direction of the drift :)

Cheers - N
 
Having run this test for a while now... I'm ready to conclude this isn't a bug, just the normal error of the crystal.

With the Teensy 4.1 on my desk right now, "mytimer.begin(pulse, 1000000.0 - 14.3);" has kept the pulse pretty close to the PPS rising edge. That correction factor of -14.3 means this Teensy 4.1's clock is running 14.3 ppm slow. That's well within the +/- 30 ppm spec.
 
That's cool. I confess I didn't do the maths initially - it just felt bad. So the solution is going to be picking an average temperature and hardcoding the correction? And then I guess I should switch to elapsedMicros for my millisecond timer so I have some resolution to scale it to account for the difference?

(This is assuming the correction needs to be done at the application level?)

Hmm, thinking aloud here

1) Count to (1000000-20)(ish) and scale the microseconds by that percentage (100.0020004%)
2) somehow distribute 20 elapsedMillis++ through each second.
3) Original option - resync to GPS PPS more frequently

4) Bonus marks, characterise the crystal performance at different temps and build a lookup table for (1) or (2)

Any of those stand out as particularly stupid or good ideas?

Cheers - N
 
LOL..

5) Use the GPS PPS signal to signal the end of every second by interrupt and multiply any microsecond time taken within that second by the percentage derived from the elapsedmillis which is reset on GPS PPS :)

Cheers - N
 
Here's a little program to automatically adjust the IntervalTimer period.


Code:
IntervalTimer mytimer;
volatile uint32_t cyclesRisingEdge;
volatile uint32_t cyclesPulse;
volatile bool gotRisingEdge=false;
volatile bool gotPulse=false;
volatile bool firstAdjust=true;
float period = 1000000.0;

void setup() {
  pinMode(2, INPUT);
  pinMode(4, OUTPUT);
  while (digitalReadFast(2) == HIGH);
  while (digitalReadFast(2) == LOW);
  mytimer.begin(pulse, period);
  mytimer.priority(192);
  attachInterrupt(2, gps, RISING);
  ARM_DEMCR |= ARM_DEMCR_TRCENA;
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;
}

void gps() {
  cyclesRisingEdge = ARM_DWT_CYCCNT;
  gotRisingEdge = true;
}

void pulse() {
  cyclesPulse = ARM_DWT_CYCCNT;
  gotPulse = true;
  digitalWriteFast(4, HIGH);
  delayMicroseconds(1);
  digitalWriteFast(4, LOW);
}

void loop() {
  if (gotRisingEdge && gotPulse) {
    float err = (int32_t)(cyclesPulse - cyclesRisingEdge) * (1000000.0f / (float)F_CPU);
    if (firstAdjust) period = period - err * 0.75;
    period = period - err * 0.02;         // integral control
    mytimer.update(period - err * 0.25);  // proportional control
    gotRisingEdge = false;
    gotPulse = false;
    firstAdjust = false;
    Serial.print("error = ");
    Serial.print(err);
    Serial.println(" us");
  }
}

file.png
 
That looks very helpful thanks. I am actually only using the IntervalTimer for testing - the variable of interest here is elapsedMillis for the millisecond portion of the stopwatch (I need to log start and stop times in 2 different locations with no comms between sites).

I'll digest the content in that example tomorrow... I am sure with the extra understanding I now have and that example I'll be able to sort something! Many thanks for the brilliant and speedy support!.

Cheers - N
 
One final comment... this control algorithm for the proportional term isn't ideal, because IntervalTimer update() effect isn't felt on the next error calculation, but on the timer's following cycle. When update() is called, IntervalTimer is already timing an interval based on the previous update(). Someone more familiar with control theory could perhaps modify this code to keep a 1 sample history of the previous error and perhaps use that info too good effect. Maybe?

If you try tuning the proportional and integral factors and see a lot of overshoot, that lag from update() to observed error is probably contributing.

A real application would probably tune those factors much lower. I picked numbers which are interesting to watch on the oscilloscope within my attention span, just is admittedly about 10 seconds. You'd probably want a much slower and more gradual adjustment for real world usage.
 
No scope here ... showing :: error = -1.12 us

With with T or F here :: firstAdjust=false; //true;

Good PPS GPS not online yet :(

... this GPS PPS has a BOUNCE - so Detach() interrupt for 600 ms after detection then re-Attach results in this with firstAdjust = true;:
Code:
T:\tCode\TIME\GPS_iValTimer\GPS_iValTimer.ino Jan 25 2021 03:09:51
error = 7.94 us
error = 14.93 us
error = 13.75 us
error = 10.57 us
error = 7.39 us
error = 4.89 us
error = 2.98 us
error = 1.55 us
error = 0.53 us
error = 0.20 us
error = -0.42 us
error = -0.68 us
error = -0.78 us
error = -0.78 us
 ...
error = -0.64 us
error = -0.63 us
error = -0.59 us
error = -0.57 us
error = -0.64 us
error = -0.71 us
error = -0.69 us
error = -0.67 us
error = -0.63 us
error = -0.62 us
error = -0.60 us

Oddly with firstAdjust=false it does this now:
Code:
T:\tCode\TIME\GPS_iValTimer\GPS_iValTimer.ino Jan 25 2021 03:18:45
error = 8.00 us
error = 15.03 us
error = 19.89 us
error = 22.66 us
error = 23.77 us
error = 23.79 us
error = 23.07 us
error = 21.86 us
error = 20.40 us
error = 18.75 us
error = 17.03 us
error = 15.40 us
error = 13.85 us
error = 12.39 us
error = 11.08 us
error = 9.87 us
error = 8.74 us
error = 7.80 us
error = 6.84 us
error = 6.04 us
error = 5.41 us
error = 4.77 us
error = 4.14 us
error = 3.59 us
error = 3.13 us
error = 2.74 us
error = 2.38 us
error = 2.08 us
error = 1.79 us
error = 1.57 us
error = 1.36 us
error = 1.15 us
error = 1.01 us
error = 0.90 us
error = 0.78 us
error = 0.65 us
   ...
error = 0.69 us
error = 0.67 us
error = 0.63 us
error = 0.59 us
error = 0.54 us
 
Here are T4 results from steady-state run of tuning sketch with NEO-M8U GPS with external antenna. Drift of this T4's 24MHz crystal is -11 ppm.
t4it.png
Offsets have small deviation, but not centered on 0?
Using mytimer.update(period - err * 0.20); // proportional control

One final comment... this control algorithm for the proportional term isn't ideal, because IntervalTimer update() effect isn't felt on the next error calculation, but on the timer's following cycle. When update() is called, IntervalTimer is already timing an interval based on the previous update(). Someone more familiar with control theory could perhaps modify this code to keep a 1 sample history of the previous error and perhaps use that info too good effect. Maybe?

If you try tuning the proportional and integral factors and see a lot of overshoot, that lag from update() to observed error is probably contributing.

A real application would probably tune those factors much lower. I picked numbers which are interesting to watch on the oscilloscope within my attention span, just is admittedly about 10 seconds. You'd probably want a much slower and more gradual adjustment for real world usage.

Another thread https://forum.pjrc.com/threads/61581-Teensy-4-1-NTP-server describes using a PID controller and GPS to discipline T4.1 clock/time. As usual changing temperature is the dominant factor affecting the T4 24-MHz crystal
 
Last edited:
I've used a one byte per second serial over USB connection from a PC to discipline a teensy audio clock to the PC system clock. Works well.
 
Back to this now I have had a chance to play around with some other ideas and I have a couple of questions about the little example program you posted Paul...

I have run it and it quickly converges does to about -0.5us which is great...

My question is really how to use this to discipline a timer. At the moment I am most of the way down the path of the following:

- Maintain a rolling average of the difference between the RTC seconds and GPS PPS as a ratio of actual to RTC seconds and then using a combination of measured differences (while PPS signal is valid) and calculated values (if PPS drops) to keep a running overall adjustment from the RTC to a computed accurate time.
- When a time stamp is needed, use some ugly code to pass the RTC UTC time, current microseconds after the RTC second tickover and the adjustment value in microseconds. The return values area corrected UTC time and corrected MS value. This I think will work, but requires all sorts of tests for negatives, rollover of seconds if the MS correction goes above 1000000 or below 0 etc. It will work, but isn't nice.

Is there a smarter way of using either your mechanism above, or the running microsecod adjustment value I have developed to adjust the RTC by just microseconds or even milliseconds? The adjustTime() seems to pass seconds which is way too coarse for my purposes.

Cheers - N
 
(Maybe) final update. I set time, take a 'time_t basetime = now()' and then just add a seconds++ into the pulse() above. Any time the program wants a time, it asks for 'local = myTZ.toLocal(basetime + seconds, &tcr);'. So far it seems to work great. I wouldn't call it elegant by any stretch but at least I understand how it's working :)

Cheers - N
 
Status
Not open for further replies.
Back
Top