Arduino as a chronometer ? Know what you are doing !

In a recent post I asked about Arduino instruction timing: my goal is to use Arduino to time periodic events (period of 1 to 5 seconds) with a desired accuracy in the range of 10 µs (15 is bearable) and I wanted to know more about how Arduino handles time measurement.
Before diving into the assembly code to look at the nitty-gritty details of the micros() instruction and the timing of interrupts I decided to code a simple timing program and to use it to check the quality of the measurements.
The signal source is a 2ppm, temperature compensated, low frequency oscillator. The signal period ranged from 1 to 16 seconds by 1-second steps, 200 measures were done for each value, the signal being also controlled with a frequency meter, showing a maximum discrepancy of .8 µs between the period displayed by the oscillator and the frequency meter measurement.
The oscillator output is connected to Arduino pin #2 and the Arduino program looks like this :

void setup()
{

attachInterrupt(0, Time_signal, RISING);

}
void loop()
{
if (sensor_flag) {
sensor_flag=false ;
Process_Time () ;
}
Record () ;
Idle() ;
}
void Time_signal()
{
time_value = micros() ;
sensor_flag = true ;
}

  • Process_Time (): process the successive values of time_value to get the value of Period ; takes into account the roll-over of micros().
  • Record(): records Period, time of day, temperature to a PC file through a serial connection.
  • Idle(): introduces an adjustable delay in the loop (nop based, does not use the delay() instruction).

Here at the results :

  1. Arduino is off, and by a wide margin given my requirements : a 1 second period is measured (averaged over 200 points) as 998,639 µs, ie more than 1,300 ppm error. Over the measurement range, 1 to 16 s, the relationship between reference time (Tr) and measured time (Tm) is precisely:
    Tm = 998641 * Tr - 2,2
    I have no way of knowing the accuracy of the crystal on my Arduino board. Cheap crystals will usually be in the 50-100 ppm range ; 1,300 ppm is clearly a surprise, even mechanical watches do better than this.

  2. For a given value of the period ( 3 s) the results are scattered over a range of 28 µs around the mean value. For the statistically oriented the standard deviation is 5. These numbers decrease as the idle time provided by Idle() increases. An intuitive explanation is that there are «collisions» that occur in the program between the real-time triggered Time_signal () function and non-interruptible segments of other program processes: in that case Time_signal () is delayed. From the outside of the program black box this looks like a random delay added to the measurement. When Idle() increases the number of nop’s in loop(), the proportion of interruptible versus non-interuptible segments increases and there will be less «collisions».

  3. And of course there’s temperature. Taking 20 °C as a reference temperature I have measured a - 16 ppm/°C drift from 15 °C to 25 °C.

In summary if you want to use Arduino for timing events set your expectations right : unless you have an external reference oscillator a granularity of 0.05 millisecond is a safe bet. And if you are looking at long term time keeping just bear in mind that counting on your Arduino only, next year you run the risk of shouting « Happy New Year » ten hours ahead of anybody else.

Thanks for this information,

It would be very informative if you could post your whole code and the device setting so anyone interested can redo these measurements e.g. with a MEGA.

Rob

So, you dont have volatile variables, thats a no no.

Depends on what time frame you do the measurement, how you measure it and the type of Arduino, the modern ones have oscillators rather than crystals. I have had a Duemilanove running a system for around 18 months. Part of that is it keeps track of it's millis() versus an NTP aligned computer. It varies slightly with temperature but generally gains around 500 milliseconds per day (~6 ppm). It keeps better time than my Casio watch. A Uno I've tested in the long term is generally within 15 seconds a day.

Here's a test for you. Insert the following code, which replaces the TIMER0 ISR, and recheck your results. The arduino TIMER0 ticks off at a 1.024 ms (vs. an implied 1ms) which creates some nasty timer/math issues. Admittedly, this code will break millis(), micros() and some PWM stuff too, but disregard that for now :wink:

Just call MyMicros() instead of micros().

//millisecond counter
volatile unsigned long my_timer0_millis;

ISR(TIMER0_COMPA_vect) {
  //incremented every 1ms
  my_timer0_millis++; 
}

//returns current microsecond [us] count
unsigned long MyMicros(void) {
  unsigned long m;
  uint8_t t;

  m = my_timer0_millis;
  t = TCNT0;
  if ((TIFR0 & _BV(TOV0)) && (t < 249))
    m++;
  return ((m*250) + t)*4);
}

void setup(void) {
  ...

  //turn off interrupts
  cli();
  
  //replace arduino timer0 code with our timer
  my_timer0_millis = 0;
  // 16Mhz/64 prescale/250 counts = 16000000/64/250 = 1000us (1ms)
  TCCR0A = 0;
  TCCR0A |= (1<<WGM01);            // CTC mode, top=OCR0A, TOV0 set @ max, update immediate
  TCCR0B = 0;
  TCCR0B |= (1<<CS01) | (1<<CS00); // F_CPU/64
  TIMSK0 |= (1<<OCIE0A);           // enable CTC A interrupt
  OCR0A = 249;                     // 249 results in a 250 count rollover

  sei();

  ...

}

Oops. Looks like you forget to disable interrupts when you read my_timer0_millis...

unsigned long MyMicros(void) {
...
// Interrupts must be disable here
m = my_timer0_millis;
// Restore interrupts here
...

I think you have to have interrupts disabled for the entirety of MyMicros.

If it makes you feel safer, you can replace MyMicros() with:

//returns current microsecond [us] count
unsigned long MyMicros(void) {
  unsigned long m;
  uint8_t oldSREG = SREG, t;

  cli();
  m = my_timer0_millis;
  t = TCNT0;
  if ((TIFR0 & _BV(TOV0)) && (t < 249))
    m++;
  SREG = oldSREG;
  return ((m*250) + t)*4);
};

for testing purposes, the previous code will work fine. Also, I think it will only run on a 168/328 based arduino running at 16MHz too.

A typical crystal oscillator will be of by up to ~200ppm. This depends on lots of factors like temperature and especially the load caps. 1300ppm is way to far of. This is more likely some issue with your code. For example blocking the timer interrupt every once in a while. If you need to keep accurate time the first thing that comes to mind is the DS3231SN (which is for example used by the ChronoDot). It will be better than 10ppm.

If you need more then a OXCO or something similar is a good choice. However at Ebay you can get used Trimble Thunderbolt modules for USD 100-200. By now I own one. This immediately solves ALL amateur timing issues. If this is not sufficient then you will not be asking in this forum anymore :wink: However it has quite some power consumption and needs clear view of the sky. But even running free it will be very much better than any Arduino or DS3231SN.

Udo

If it makes you feel safer,

My feelings are irrelevant. The correctness of your code is. Thank you for posting the update.

Thank you all for your comments, including the code suggestions that I will try. To answer some of the questions that were asked :

  • Arduino Uno board, Atmega 328, 16 Mhz, made in Italy. No other useful information and the «zero carbon footprint» mention certainly does me a lot of good !
  • Quakko 5000 signal generator
  • HP 5316B universal counter
  • I have not quoted the whole code as neither variable names nor comments are in English, ie not very useful for most people on this forum. However the period computing function is exactly this :
  • ==========
    void Process_time()
    {
    if (time>time_old) // will skip next instructions if micros() just rolled over
    {
    period = time_old – time // compute period as the difference between current time and old time
    time = time_old // current time stored into old time
    Record()
    }
    time, time_old and period are global, declared as long unsigned.
    Record() is actually launched by Process_Time() and is not in loop() as I indicated in my first post. It gets temperature from an analog sensor and then uses Serial.print to send values to the PC.

I know that 1300 ppm error is a lot, as I said my expected worst case was around 100. I did think about blocked interrupts, there might be some, and I believe they are responsible for the 28 µs dispersion around the average measurement; but blocked interrupts that set the count back by 1300 µs ? There I scratch my head !

So… next steps :

  • Experiment with the code that was suggested
  • Directly measure the crystal frequency (maybe tricky, depends on what probe I can get)
  • Replace the current crystal with a 10 ppm one
  • Use an accurate RTC, that was part of the plan anyway as I explained in my first post on the subject.

Any particular reason you're not using the Input Capture unit that's available on Timer 1?

No, actually I just wanted to go step by step, checking first what the "standard" programming as described in the Arduino Reference had to offer, and move to the next level only if I could not reach my required accuracy.

I guess that's exactly where I am now, so I'm going to use all the advice that you and others have suggested.

To be honest just before I do that I'll change the crystal on the board. Don't ask me why I suspect the animal, it's just an intuition and it won't cost me more than 10 minutes anyway.

At this time unfortunately we need to report that after receiving Dr JimEli's medicine, ie MyMicros() replacing micros(), the patient is still suffering from the same 1300 or so ppm error.

We thank (again) the good doctor for his advice, as trying his stuff actually helps us narrow down on the guy we suspected from the very beginning, the 16-ish Mhz crystal.

Dr Soldering Iron is impatiently waiting for his turn, and of course we don't forget the Timer1 cure of Dr Coding Badly. But right now it's going to be a good-cop bad-cop routine with oscilloscope and counter to see what this crystal is really saying.

I’m happy to report that the culprit has been identified: it’s the oscillating circuit.

I measured the oscillator frequency using the following methodology:

  • Arduino running the simple Blink program (http://arduino.cc/en/Tutorial/Blink), with 500 ms on, 500 ms off.
  • Oscilloscope + frequency counter with: probe 1 on Arduino pin 13, ie the 1 Hz signal to the LED; probe 2 on Atmega pin 9, ie the internal oscillating amplifier output to the crystal.
  • All frequency and period measurements averaged over 3 minutes.
  • First series of measurements with probe 1 connected only. The oscillator runs at its normal frequency, frequency_unloaded, unknown. We measure the period of output 13, period_unloaded.
  • Second series of measurement with probe 1 + probe 2 connected. The oscillator is loaded with the capacitive charge of probe 2 (15 pF) ; we measure its new frequency, frequency_loaded as well as the new period of output 13, period_loaded.

Here are the results:
Period_unloaded = 1,003,595 µs
Period_loaded = 1,00,1726µs frequency_loaded = 15,942,950 Hz
From which we derive frequency_unloaded = 15,973,908 Hz
Compared to the nominal 16 Mhz figure that’s a 1600 ppm error.

Conclusion: as stated earlier interruptions may «collide» with non-interruptible code, in my case this would result in variations of the measured period from measurement to measurement, small variations however.
What we have here is a lousy oscillator. This can be blamed on any combination of crystal accuracy, badly chosen capacitive load, oscillator layout on the PC board etc.

Ah, yes, but it’s «zero carbon footprint», who can beat that ?

But do not measure directly at the crystal. The probe loads will shift the frequency a lot.

Here are the results:
Period_unloaded = 1,003,595 µs
Period_loaded = 1,00,1726µs frequency_loaded = 15,942,950 Hz
From which we derive frequency_unloaded = 15,973,908 Hz
Compared to the nominal 16 Mhz figure that’s a 1600 ppm error.

I think you made a mistake...

The period going from unloaded (1,003,595) to loaded (1,001,726) DECREASED. For that to happen, the processor clock frequency has to INCREASE.

In your derivation, the frequency going from unloaded (15,973,908) to loaded (15,942,950) DECREASED. The frequency changed in the wrong direction.

You are right and I thought I was very stupid not to realize that.
So I recomputed everything, getting to a new unloaded frequency that was totally incompatible with the results I quoted yesterday. Now I felt even more stupid. Then I looked at my notes on paper and concluded that, yes I was definitively stupid: I switched the loaded and unloaded numbers when I copied them to the screen.
The good news is that all results are consistant again, I guess you can call that consistant stupidity.

update?

The update is pretty much what you can see above:
1- tested your code, no improvement.
2- focused on the oscillator: it's off by 1600 ppm!
No programming feat can cure that.

Next step: include a DS3231 RTC or similar in my design: it will give me a 2ppm, temperature compensated, 1-second signal against which I can calibrate every Arduino time measurement.

I'm actually working on a project with similar requirements to yours -- I'm building a GPS-disciplined NTP server on an Arduino (not for any serious industrial need, just to see how good of a job I can do), so of course the core of it is an adjustable timekeeping loop using one of the AVR's hardware timers.

I decided to leave alone the TIMER0 which is used by the arduino library's micros() etc. and instead programmed TIMER4 myself (I've got a Mega2560 board, which has the ICP4 pin routed but not ICP1; a 2009 should work as well if everything is changed over to use TIMER1, since that board has ICP1 routed but there is no ICP4). It pre-divides the 16MHz clock by 8 for 0.5uS counter resolution, and uses a timer overflow value of (nominally) 62500 to give 32 interrupts per second. By varying that value +/- 127, I can get +/- 2048 ppm speed adjustment in units of 16 ppm, but by dithering I'm able to make the adjustment accurate to +/- 1 ppm over a period of 0.5 sec and +/- 1/16 ppm over a period of 8 sec.

Coupled to that is a PLL that locks to a 1PPS input signal connected to the ICP4 pin (substitute ICP1 for the 2009), and the code is able to timestamp events on the input capture pin as well as read the current time from the counter.

Consistent with what you saw, I observed about a 24uS jitter associated with having another interrupt delayed by my timer ISR. Using the input capture pin can mitigate this, but since there's only one ICP per timer, you'd need to multiplex it cleverly if you wanted to use it for external events and for locking to the 1PPS.

Also somewhat consistent with what you saw, the crystal on my board runs about 240ppm fast at room temperature, and it's very temperature-sensitive, with a constant of about 15ppm/degC. I've got a 1-wire temperature probe wired up and taped to the crystal, and I'm working on putting temperature compensation ahead of the PLL, but it's a little bit fiddly and I haven't finished with that yet.

My code is admittedly fairly scary and I haven't reached the point where I was ready to show it to the world yet, and the PLL is really ham-fisted because this is NOT my specialty, but if you want to have a look at any of it, it can be found at http://cleverdomain.org/git/arduino/HEAD/tree .