[solved] Highly accurate Arduino time via RTC pulse to update seconds?

I found a great post here: "How to get a more accurate RTC clock set from a NTP time server"
Wonderful! I love accurate clocks...

I Implemented the NTP modification to get the fractional time part (milliseconds)

Then I wired up my DS3231's SQW pin to an interrupt.
Now I have on the FALLING edge exactly the start of a new scond

I thus SET the Arduino time_t t at that precise moment using RTC time so I am not dependent on the oscillator of the Arduino

It does work but I'm learning a lot with this project so my only question is now of how to improve my way of doing this because I think it's clumsy: Is there a better way to keep close to the Time AND Timezone libraries way of keeping time and still dictate the timing via the RTC SQW pulse??

What I now do is checking the now() time and every 60 seconds the arduino time_t t will be updated with the RTC time -->at the exact moment a new (RTC) second starts.

Q: Is it possible to use the RTC second pulse to continually keep the 'internal' arduino timer/ seconds in sync??

My code is part of a large project, very long and very unfinished (but does work)
So maybe it's enough to describe the steps and parts of the code?
If not please let me know.

Arduino Nano 33 Iot - SAMD21 MC

Main libraries:

#include <Arduino.h>// included for PlatformIO IDE
// Wifi
#include <WiFiNINA.h> // arduino-libraries/WiFiNINA@1.8.13
#include <WiFiUdp.h> // arduino-libraries/WiFiNINA@1.8.13
#include "wifi_secrets.h"
// Includes for Time
#include <TimeLib.h> //	paulstoffregen/Time@1.6.1
#include <Timezone.h> // jchristensen/Timezone@1.2.4
// RTC
#include <DS3231.h> // northernwidget/DS3231@^1.1.0

At startup I sync the RTC with NTP time at millisecond accuracy.
(NTP function listed at the end of this post)

  // set the RTC with NTP time
  myClock.setEpoch(getNtpTime());

Setup: SQW and interrupt attachment:

  // enable DS3231 Oscillator.  Parameters:
  // true:Oscillator ON, true:also when on battery, 0:frequency 1 Hz
  myClock.enableOscillator(true, true, 0);

  // attach the DS3231 Interrupt routine (use EXTERNAL pullup!)
  // a new RTC second will start on the falling edge!
  //
  // Using this as the timing instead of the internal millis()
  // will give amazing accuracy: only a few seconds/year
  // (only if you use a GENUINE DS3231 chip of course...)
  // Update via the NTP server once a month is more than
  // enough to have always accurate time.
  attachInterrupt(digitalPinToInterrupt(DS3231_1Hz_PIN),
                  secondInterrupt, FALLING);

Interrupt routine: called function:

void secondInterrupt()
{
  newSecond = true;
}

Loop(): this function is called constantly:

void syncTimeWithRtcPulse()
{
  // newSecond is set by the RTC SQW pulse via an 1Hz interrupt
  if (newSecond)
  {
    newSecond = false;
    // set the time every 60 seconds
    if ((now() % 60 == 0) || (coldStart == true))
    {
      // coldStart only executes at power on or reset
      coldStart = false;

      // we first read the time t from the RTC
      DateTime nowRTC = myRTC.now();
      //next set the arduino time
      setTime(nowRTC.unixtime());
    }
  }
}

changed part of NTP code (full NTP code below that):

      // convert four bytes starting at location 44 to a long integer
      // this is the fractional part of the NTP time info
      fractionalPart = (unsigned long)packetBuffer[44] << 24;
      fractionalPart |= (unsigned long)packetBuffer[45] << 16;
      fractionalPart |= (unsigned long)packetBuffer[46] << 8;
      fractionalPart |= (unsigned long)packetBuffer[47];
      fractNtp = ((uint64_t)fractionalPart * 1000) >> 32;

      // compensate for the fractional time AND detract
      // 50 ms for round trip delay. works for me!
      delay(950 - fractNtp);

      // add one second to compensate for delay 
      return secsSince1900 - seventyYears + 1;

full NTP code

/*-------- NTP code ----------*/

const int NTP_PACKET_SIZE = 48;     // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets

time_t getNtpTime()
{
  // ! Time zone >> NOT USED becaue we use the Timezone library
  const int timeZone = 0;

  // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
  const unsigned long seventyYears = 2208988800UL;

  IPAddress ntpServerIP; // NTP server's ip address

  while (Udp.parsePacket() > 0)
    ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  // get a random server from the pool
  WiFi.hostByName(ntpServerName, ntpServerIP);
  Serial.print(ntpServerName);
  Serial.print(": ");
  Serial.println(ntpServerIP);
  sendNTPpacket(ntpServerIP);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500)
  {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE)
    {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE); // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 = (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      // get fractional time - milliseconds
      unsigned long fractionalPart;
      // convert four bytes starting at location 44 to a long integer
      // this is the fractional part of the NTP time info
      fractionalPart = (unsigned long)packetBuffer[44] << 24;
      fractionalPart |= (unsigned long)packetBuffer[45] << 16;
      fractionalPart |= (unsigned long)packetBuffer[46] << 8;
      fractionalPart |= (unsigned long)packetBuffer[47];
      fractNtp = ((uint64_t)fractionalPart * 1000) >> 32;

      // compensate for the fractional time AND detract
      // 50 ms for round trip delay. works for me!
      delay(950 - fractNtp);

      // add one second to compensate for delay 
      return secsSince1900 - seventyYears + 1;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}

Possibly, but why bother?

Doing so depends on the details of how the Nano 33's internal clock is maintained. Have you investigated that?

If you want really precise seconds, use the PPS output of a GPS module.

Thank you for your reply!

I don't know much about the internal 33 IoT clock. It has an internal RTC which is very inaccurate. Other than that, it will be way above my technical skills to investigate that.

The output of the DS3231 has a precision of -way less- than 60 sec/year. And obviously an GPS is more expensive to use...

I only bother because I want to learn how to do thing better so I hoped someone could help.

If you want to fiddle with the internal timekeeping of the Nano 33, then you have no option other than to look at the code that maintains the clock. I would be surprised if anyone on the forum has done that, but who knows?

An alternative is to use the seconds interrupt from the DS3231 SQW output to keep track of time in your own code. That way you do not need to read the RTC to keep track of time, and have no synchronization problem. The time you keep will be exactly as accurate as the DS3231 time.

Here is one very simple approach, which maintains a time-of-day clock in BCD (binary coded decimal) format based on a once per second tick:

char dayno=0, RTC_buf[6]={0}; //global clock

void bcd_tod () {  //called on timer tick, once per second

  // RTC function, keep time in BCD binary HH:MM:SS, and days since startup

  RTC_buf[5]++; // increment second

  if (RTC_buf[5] > 9)
  {
    RTC_buf[5] = 0; // increment ten seconds
    RTC_buf[4]++;
    if ( RTC_buf[4] > 5)
    {
      RTC_buf[4] = 0;
      RTC_buf[3]++; // increment minutes
      if (RTC_buf[3] > 9)
      {
        RTC_buf[3] = 0;
        RTC_buf[2]++; // increment ten minutes

        if (RTC_buf[2] > 5)
        {
          RTC_buf[2] = 0;
          RTC_buf[1]++; // increment hours
          char b = RTC_buf[0]; // tens of hours, handle rollover at 19 or 23
          if ( ((b < 2) && (RTC_buf[1] > 9)) || ((b == 2) && (RTC_buf[1] > 3)) )
          {
            RTC_buf[1] = 0;
            RTC_buf[0]++; // increment ten hours and day number, if midnight rollover
            if (RTC_buf[0] > 2) {
              RTC_buf[0] = 0;
              dayno++;  //count days since startup
            }
          }
        }
      }
    }
  }
}
1 Like

That would be a good solution, thank you for that! In that case I'll have to take care of summertime without the Timezone library? I'll look into that.

Write some simple code to transform the BCD/dayno variables into a unix timestamp, and you can do any timezone/DST transformations you wish, using TimeLib.h.

1 Like

Appreciate your help, thank you!

Another option is use the 32kHz output of the DS3231 to clock the internal RTC of the SAMD21. But unfortunately, the clock topology of the SAMD21 is very complicated...

1 Like

The way I handled fractional time, is to count milliseconds from the PPS reference, using only the CPU timer (millis). Since it's refreshed every second, it has only to exceed 1000 PPM to stay in sync (extremely easy to achieve).

The PPS was sampled on an interrupt pin, so the ISR just captured the value of millis(). Subsequently, a comparison of current millis with that, gives you the fractional time.

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.