Making Arduino timing more precise

Clock

I often see people want a RTC connected to their Arduino to get more precise time, but that is not always necessary. Here I will look at how precise the Arduino is and how to get the best possible time precision.

Contents
    Making Arduino timing more precise
        Specifying time precision
        What is a RTC and where to use it
            What is a RTC
            Where to use it
            Examples on RTC chips
        How to calibrate the time on Arduino
            Crystals
            How to apply adjustment
            How to measure amount of adjustment to apply
            Is the Arduino time now precise?
    Software
        The library (PrecisionTime)
            Using the library
            Interrupt handler and second counter
            Interrupt configuration and routine
            Clock with seconds, minutes, hours, date, month, year
            PC calibration interface
            Checking for calibration
        The Arduino sketch
        PC program
        Testing RTC's
    Supported processors
        Arduino UNO (Mega328)
        Arduino Mega (Mega2560)
        Arduino Nano (Mega168 & Mega328)
        Seeeduino Nano (Mega328)
        ProMicro simulating Arduino Leonardo (Mega32U4)
        Nano Every (ATMega4809)
        Other
    Tested RTC's
        DS1302
        DS1307
        DS3231
        PCF8563
    Conclusion
    Notes and download

Specifying time precision

How good precision can be expected?

Types

The above table shows some of the different technologies used to get a stable frequency or time. The data are a rough specification for that type of technology, there will be both better and worse devices around.

Precision

Here are some of the values used to specify time precision with a conversion between them.



What is a RTC and where to use it

Lets first look at what a RTC (Real Time Clock) is and in what applications a RTC is necessary.

What is a RTC
A typical RTC consist of a chip, a battery and a tiny 32768Hz crystal. The 32768Hz crystal is a typical watch crystal, i.e. it is used in many different types of electronic clocks, there are two reasons for the frequency: The chip will count seconds and organise the result in seconds, minutes, hours, days, months and years. Because the chip is battery backed it will often have a small amount of extra non volatile ram that can be used by a microprocessor.


Where to use it

With a battery, low power consumption and independent operation it is perfect for keeping time when a device has other stuff to do and in some cases it may improve the precision. In the Arduino world this translates to:

Examples on RTC chips

DS1302

DS1302: This is a simple RTC chip with the crystal, input for battery and normal power, option to charge battery, date and time registers and a few bytes spare ram.

PCF8563

PCF8563: Another simple RTC chip with one alarm (Can be used to bring a processor out of sleep), but only a single power input (Combining battery and normal power must be done externally), no battery charging supported.

DS3231

DS3231: This chip is more advanced and also have two alarms (Can be used to bring a processor out of sleep). It has temperature and age compensation of the oscillator (This makes it a TCXO), this is possible because the crystal is build into the chip. Specified to be within 2 minutes a year.



How to calibrate the time on Arduino

The Arduino often use a cheap crystal or ceramic resonator and is not calibrated, due to this the clock frequency will usually be a bit beside the 16000000 ticks a second (16MHz clock), but it will be fairly stable. This means that adding or subtracting some time at regular intervals will make the time more precise. There is a few challenges with this:

Crystals

A007%20-%2019700101_010000s A008%20-%2019700101_010000s

Two "crystals" from Arduino processors. The first one is probably a ceramic resonator (they work the same ways as crystal, but are cheaper). See chapter about Arduino boards for more examples.

A006%20-%2019700101_010000s

On UNO, Mega and some other there is a extra crystal, it is usual marked 12MHz. This is not for the Arduino processor, but for the USB interface and must be precise enough for that.



How to apply adjustment

The usual way to make a timer is to use a hardware time and have a interrupt to count at specific interval. Lets say every millisecond, 16000000Hz (Arduino crystal) to 1000Hz is possible on a Arduino hardware timer. When the counter reach 1000 a second is passed and the second register can be incremented. If we increment the second register when reaching 999 instead of 1000 the seconds will run 0.1% faster, we could also count to 1001 for 0.1% slower. Only problem with this is the 0.1% resolution, it is not very precise, more exactly it is 86.4 seconds in a day. What if we add 10 to the counter register each millisecond and when it reaches 10000 we subtract 10000, this will give exactly the same speed, but subtracting 9999 or 10001 is 0.01% or 8.64 second a day, this is much better. In my solution I do not use a 1000 or 10000 counter, but a 8640000 counter, i.e. interrupt adds 8640, this may look like a strange number, but is very practical to calculate on because there is 86400 seconds in a day and by adding two extra zeros each time I add or subtract one it will be 0.01 second a day.
This means if I measure the Arduino looses 15.32 seconds a day, I simply subtracts 8640000-1532-> 8638468 from the counter each second instead of 8640000 and the seconds will count correctly.

The interrupt do not have to be 1000 times a second, but using it means the length of a second will be adjusted in 1/1000 of a second steps when correcting.



How to measure amount of adjustment to apply

If there is a human in the measurement loop there will be a tolerance of 0.3 second to a couple of seconds depending on the user interface, this means the calibration time must take a couple of days to get precision around a second a day, this is not very practical.
Instead I connect the Arduino to a PC and let the PC measure, there I can get around 0.005 second tolerance, this means a 15 minute calibration time is enough to get a fairly good calibration (Within one second a day), longer time will give a better calibration.
But how to do it, on the Arduino I work in seconds and if I just read the seconds I will have a tolerance of a full second? The idea is to look for a change, i.e. read the second values multiple times until it change, then store it together with the PC's millisecond value (That is fairly precise). Doing it again some time later and I can calculate the speed of the Arduino with 1 millisecond resolution for that time interval.
With that it is fairly easy to calculate time error in a day and use it as a correction factor to the 8640000 from above.
To get best possible precision the millis interrupt is disabled during calibration.



Is the Arduino time now precise?

The answer is no, firstly it is calibrated to the PC clock and that is not perfect. On the Arduino the oscillator will have variation based on: The above list applies both to RTC modules and to the crystal on the Arduino.

It is not possible to fix all the above factors, but minimizing them will improve the precision and remember to do the calibration at the actual voltage and temperature the Arduino is used at for best result. Use a Arduino with a crystal for best results.
Also remember that the Arduino is calibrate to the same clock speed as the PC, i.e. it will not be better than the PC clock.



Software

The software is in 3 parts, a Arduino library, a Arduino sketch and a PC program.



The library (PrecisionTime)

The library is called PrecisionTime and has a couple of different parts, it is not requires to use all of them. The compile will remove the unused part, this means less flash memory requirement. The parts are: Only the first is mandatory to include in your programs. The second can be included or you can make your own.



Using the library

Code:
#include "PrecisionTime.h"

PrecisionTime precisionTime(PRECISIONTIME_EEPROM_ADDRESS);

PRECISION_TIMER_INTR(precisionTime)
The library is simple to use, include it, instantiate it, the parameter is where in the EEPROM it saves the calibration factor, you can use the supplied constant or supply your own address. Last add a interrupt handler.



Interrupt handler and second counter

This is the mandatory part, that must be called 1000 times each second, it will apply the adjustment and count seconds.

Code:
byte getDeltaSeconds();
This routine returns how many seconds since it was last called, this is the correct time. There is two ways to use this:

Code:
void loop() {
  ...
  precisionTime.update();
  ...
}
Either call the update function in the main program loop to maintain a seconds, minutes, hours, ... counter

Code:
void loop() {
  ...
  mySecondCounter+=precisionTime.getDeltaSeconds();
  ...
}
Or maintain your own second counter.
The function returns a byte sized answer, this means it must be called at least once every 4 minutes or it will loose time.

Code:
updateIntr();
This function must be called 1000 times each second, if the interrupt part is included this is handled by the library. Without the interrupt part included you must supply your own interrupt and call this routine 1000 times a second. If it is called slightly faster or slower it will still be possible to calibrate it.



Interrupt configuration and routine

The interrupts do not require any coding, but there must be a call to the interrupt handler in your code.
Code:
PRECISION_TIMER_INTR(precisionTime)
This is done with this define, it must be placed outside any function.
The library support from timer1 to timer5 on Arduino and must have exclusive use of that timer. The default is to use timer1

Code:
//#define _USE_TIMER0_   // Supported on Mega4809(B0)
#define _USE_TIMER1_   // Supported on Mega328 & Mega2560 & Mega32U4 & Mega4809(B1)
//#define _USE_TIMER2_   // Supported on Mega328 & Mega2560 & Mega4809(B2)
//#define _USE_TIMER3_   // Supported on Mega2560 & Mega32U4 
//#define _USE_TIMER4_   // Supported on Mega2560 & Mega32U4 
//#define _USE_TIMER5_   // Supported on Mega2560
In PrecisionTime.h is the above lines, only one of them must be uncommented and will include code for that timer. If all 5 lines are commented out no timer code is included and you must supply your own timer interrupt 1000 times a second and it must call updateIntr();

Code:
void setup() {
  precisionTime.begin();
}
To start the timer interrupt begin must be called.



Clock with seconds, minutes, hours, date, month, year

C/C++ has a useful time structure (struct tm) and time value (time_t), but they uses more than 1kbyte flash, to keep the memory size small I have defined a second, minute, hour, date, month, year structure with byte sized variables (except year), that can be maintained by the library

As default it is included, but can be removed by adding two backslashes before this line in the PrecisionTime.h header file:
Code:
#define _INCLUDE_TIME_ 

Code:
void loop() {
  ...
  precisionTime.update();
  ...
}
For this timekeeping to work the update function must be called at regular intervals.

Code:
Time getTime();// Get the time structure

// Get the individual fields, these function are both available in PrecisionTime and Time. 
byte getSecond();
byte getMinute();
byte getHour();
byte getDay();
byte getMonth(); 
int getYear();

// Adjust the individual fields, for leap year to work date, month and year must be initialized
// These are only available in PrecisionTime.
void setSecond(byte second) {
void setMinute(byte minute) {
void setHour(byte hour) {
void setDay(byte day) {
void setMonth(byte month) {
void setYear(int year) {
There are function to read and set the time when using it, they are both present in the Time structure and the PrecisionTime class.
The time will not change while accessing these functions, it will only change when update is called.

Code:
struct tm;
time_t cTime=precisionTime.getTM(&tm);
With the above function it is possible to get both the standard C structure and time value, but it will require some flash memory for the code.



PC calibration interface

The calibration interface uses the serial port for communication with the PC, this is not something that requires much code to use.
Code:
void setup() {
  precisionTime.begin();
  Serial.begin(9600);
  Serial.println("Test precision time");
}
There must be code to initialize the serial port and optionally print some text.

Code:
  if (Serial.available() > 0) {
    switch (Serial.read()) {
      case '#': precisionTime.startCalibration(); break;
    }
  }
Then there must be a way for the PC software to activate the calibration part, the PC does that by sending some characters, default is a hash (#) character, but it can easily be change in the calibration program. When this character or command is received the startCalibration must be called, everything else is handled by the library. If the software is using interrupts the calibration may be faster if they are disabled before calling startCalibration.
Code:
startCalibration();
startCalibration(baudrate);
There is two versions of startCalibration, the default assumes 9600 baud, the other can have a baudrate as parameter and will return the serial port to this baudrate after doing the calibration.

Code:
startMillisCheck();
startMillisCheck(baudrate);
Instead of startCalibration it is also possible to call startMillisCheck, it works the same way, but will use the millis function for time and cannot be calibrated. This function do not require any timer to be enabled, i.e. it can be used on any Arduino with any processor as long as it has a serial class. This can be used to see how fast/slow the clock/millis runs on that Arduino using the PC program.



Checking for calibration

To get any advantage of the library the calibration must be performed, there is a function to check if this calibration is performed:
Code:
boolean isCalibrated();
It check if the calibration factor is different from zero.



The Arduino sketch

This is provided as an example, and can be used to calibrate a Arduino with 168/328/2560/32U4/Mega4809 processor. It is possible to calibrate with this sketch and then use a main sketch without any calibration, only requirement is the inclusion and usage of PrecisionTime library in both and using the same EEPROM address.

Code:
#include "PrecisionTime.h"

PrecisionTime precisionTime(PRECISIONTIME_EEPROM_ADDRESS);

PRECISION_TIMER_INTR(precisionTime)

void setup() {
  precisionTime.begin();
  Serial.begin(9600);
  Serial.println("Test precision time");
}

void loop() {
  precisionTime.update();

  if (Serial.available() > 0) {
    switch (Serial.read()) {
      case '#': precisionTime.startCalibration(); break;
    }
  }
}
It is very simple. Remember to change the EEPROM address if the actual Arduino program needs to use another address.

The included sketch is slightly more complex, it can time millis time and it can show time on the terminal.

Code:
#include "PrecisionTime.h"

PrecisionTime precisionTime(PRECISIONTIME_EEPROM_ADDRESS);

PRECISION_TIMER_INTR(precisionTime)

void setup() {
  precisionTime.begin();
}

void loop() {
  precisionTime.update();
}
Minimum sketch to maintain a clock with the library, there must, of course, be added ways to set the time and read the time.

Code:
#include "PrecisionTime.h"

unsigned long mySecondCounter=0;
PrecisionTime precisionTime(PRECISIONTIME_EEPROM_ADDRESS);

PRECISION_TIMER_INTR(precisionTime)

void setup() {
  precisionTime.begin();
}

void loop() {
  mySecondCounter+=precisionTime.getDeltaSeconds();
}
Implementing a precise second counter, calibration must be done with the test sketch.



PC program

For fastest and most precise calibration the PC program requires a PC with very few programs running during the calibration. It is possible to calibrate on a busy PC, but it will take longer to get a stable calibration. It is also possible to connect multiple Arduinos and use multiple instances of this program to calibrate more at the same time.
The program is in Java and requires that Java is installed.

Test3s

When the program is started the serial port and baud rate must be selected and the "Calibration init string" adjusted to the one actually used, for the test sketch the # is correct.
If anything is written in the "Log file" field a log file will be created with the time of the PC and Arduino during calibration.
First program the Arduino from the Arduino IDE, but do not open the terminal, then press Start.

Test4s

The software will switch the Arduino into the PrecisionTime calibration routine and change the baud rate. If anything fails it will abort.
The lines starting with "Rx" is random messages received from the Arduino.
Current calibration factor is the current value stored in the Arduino or 0.0 for the first run or if EEPROM location is changed.
Pressing Leave calibration will tell the Arduino to exit the calibration routine and will close the serial port on the PC (i.e. the Arduino IDE can then access it).
The status line will shown how long the calibration has been running and initially only the difference between the PC and the Arduino.

Test6s

When the calibration has been running for some time and the time difference between the PC and Arduino is large enough the status line will include a estimate of the daily time error. The +/- value is how much the daily error varies during the last 20 measurements, this need to be low for a good calibration.

Test7s

After some time the Calibrate button will be enabled. The time will vary from 30 seconds to a couple of minutes, depending on how stable the estimates are. With a already calibrated Arduino it may take much longer before the Calibrate button is enabled.
The calibration button will only be enabled if calibration is possible, when testing RTC's it will never be enabled.

Test8s

Pressing the Calibrate button will transfer the calibration factor to the Arduino, that will save it in EEPROM, then the Arduino will leave the calibration function and the PC will close the serial port.
It is possible to do multiple calibration on the same Arduino, the calibration factor will be adjusted correctly.

Test1s

Here a Arduino is calibrated after 1 hour where it was about 1 second to fast.

Test2s

Starting over and running for more than 9 hours (580 minutes) the calibration has mostly removed the error, it is only about 0.1 second to fast in the 9 hours and a estimated 0.3 second a day.

Test9s

Using "m" instead of "#" with the test sketch will use the millis function for time, i.e. show the uncalibrated values and it is not possible to store a new calibration.



Testing RTC's

The PC program can also be used to test RTC's, this requires another sketch that will fetch the seconds from the RTC instead of using the millis or the library. This sketch is included.

Code:
// Select one RTC chip 
//DS1302 rtc(A3,A5,A4);  // Note pin assigment: A3:CE/RST, A5:CLK, A4:DAT

// A4:SDA, A5:SCL on UNO and Nano
//DS1307 rtc;  
DS3231 rtc;   
//PCF8563 rtc;
//PCF8583 rtc;
Uncomment the used RTC chip and connect it to the pins listed, then upload it to the Arduino.

The PC program is used the same was as above with the "Calibration init string" being #. The calibrate button will not be enabled, it will only list the timing erros.



Supported processors

The calibration in the table is seconds a day a clock would drift, I measured over one hour to get good precision. I have marked the processor crystal/resonator on the pictures. There is a line in the tables for each unit I have tested.
All tested boards are cheap versions from Ebay or Aliexpress, except Nano Every.



Arduino UNO (Mega328)

DSC_7955
DSC_7953 DSC_7954

UNO

This processor support Timer1 & Timer2
There are two crystals on this board, a very small one for the processor (Probably a ceramic resonator) and a larger one for USB. As can be seen from the table the processor has a large deviation on the clock.

DSC_8981

DSC_8998 DSC_8983

UNO2

A few places sells another UNO with a crystal for the processor.



Arduino Mega (Mega2560)

DSC_7952
DSC_7950
DSC_7951

Mega2560

This processor support Timer1, Timer2, Timer3, Timer4  & Timer5
There are two crystals on this board, a very small one for the processor (Probably a ceramic resonator) and a larger one for USB. As can be seen from the table the processor has a large deviation on the clock.



Arduino Nano (Mega168  & Mega328)

DSC_7958
DSC_7956
DSC_7957

Nano168

There are two crystals on this board, a very small one for the processor (Probably a ceramic resonator) and a larger one for USB. The frequency is not precise enough for clocks, but not bad either.

DSC_7964
DSC_7962
DSC_7963

Nano328

These processors support Timer1 & Timer2
On this board there is only one crystal because the USB chip has a build in oscillator (On CH340C, CH340E and CH340B), but there is space for an external one near C4 if using another chip (CH340G, CH340T and CH340R). The frequency is not precise enough for clocks, but not bad either.



Seeeduino Nano (Mega328)

DSC_1956
DSC_1961
DSC_1962

SeeeduinoNano

This is a Nano clone, but with some improvement: USB-C connector, Grove connector and delivered with pins soldering in. It is from Seeed studio.



ProMicro simulating Arduino Leonardo (Mega32U4)

DSC_7961
DSC_7959
DSC_7960


ProMicro

This processor support Timer1, Timer3 & Timer4
This board has one crystal, because the 32U4 processor includes the USB interface. I am rather impressed with this precision, but it is still not perfect for a clock.

DSC_8984
DSC_8985
DSC_8986

ProMicro2

This ProMicro has something that looks like a ceramic resonator, but precision is as good as a crystal.



Nano Every (ATMega4809)

DSC_9544

DSC_9542
DSC_9543

NanoEvery

This processor do not use a crystal or resonator, but an internal oscillator in the processor. This means very bad clock precision and high sensitivity to temperature and probably also voltage. This means it is important to do any calibration at the same temperature and voltage as the processor is going to work at.



Other

I have not tested with other boards, but I would expect the full library to work on any Arduino based on a Mega processor.
For Arduinos with non-mega processors the user must setup the 1000 interrupts a second and call the updateIntr(), as long as the processor supports EEPROM the rest will work.



Tested RTC's

The precision is seconds error in a day measured with the application, i.e. the precision of the PC clock will affect the result.
All tested boards are cheap versions from Ebay or Aliexpress.



DS1302
DSC_9212

DSC_9213
DSC_9214

DS1302



DS1307
DSC_9208 DSC_9209

DSC_9210 DSC_9211

DS1307



DS3231

DSC_9069 DSC_9070

DSC_9071
DSC_9072


DSC_9073 DSC_9074


DSC_9075 DSC_9076

DS3231

This RTC register about 0.5 second error in a day, some of this (or maybe most) is from the PC clock.



PCF8563

DSC_9066

DSC_9067
DSC_9068

PCF8563

These RTC's are a few seconds too fast each day.



Conclusion

This library makes it very easy to get good time precision on a Arduino, the limits are the precision of the PC time and how stable the crystal/ceramic resonator is. In praxis I can get down to about 1 second a day precision with my PC.
It has many options on how to use it, either with a calibration build into the user program or with a separate calibration sketch. It can maintain a clock, even with standard C structures or the user can just use the precise seconds.
Most RTC's will not be better and often is slightly worse than a Arduino with a crystal that is calibrated, the exception is RTC's based on DS3231, they are very precise. A Arduino with calibrated ceramic resonator can also be fairly precise, especially if the temperature is stable.



Notes and download

The calibration factor is specific to each Arduino and has nothing to do with selected interrupt, i.e. calibration will be valid even if interrupt is changed.

Precision time library with examples
Test RTC sketch
PC program
PC program source code