Go Down

Topic: Arduino as a chronometer ? Know what you are doing ! (Read 5804 times) previous topic - next topic


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 Tillaart

Nederlandse sectie - http://arduino.cc/forum/index.php/board,77.0.html -
(Please do not PM for private consultancy)


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.  


Mar 01, 2011, 09:47 pm Last Edit: Mar 01, 2011, 09:54 pm by JimEli Reason: 1
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 ;-)

Just call MyMicros() instead of micros().

Code: [Select]

//millisecond counter
volatile unsigned long my_timer0_millis;

 //incremented every 1ms

//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))
 return ((m*250) + t)*4);

void setup(void) {

 //turn off interrupts
 //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




Coding Badly

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.


Mar 01, 2011, 09:59 pm Last Edit: Mar 01, 2011, 10:55 pm by JimEli Reason: 1
If it makes you feel safer, you can replace MyMicros() with:

Code: [Select]

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

 m = my_timer0_millis;
 t = TCNT0;
 if ((TIFR0 & _BV(TOV0)) && (t < 249))
 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.

Udo Klein

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 ;) 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.

Check out my experiments http://blog.blinkenlight.net

Coding Badly

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
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.

Coding Badly

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 ?

Udo Klein

But do not measure directly at the crystal. The probe loads will shift the frequency a lot.
Check out my experiments http://blog.blinkenlight.net

Go Up

Please enter a valid email to subscribe

Confirm your email address

We need to confirm your email address.
To complete the subscription, please click the link in the email we just sent you.

Thank you for subscribing!

via Egeo 16
Torino, 10131