Precision Timekeeping with Arduino and DS3231

Protonest IoT
6 min readAug 30, 2024

--

When it comes to embedded systems, there are applications where accurate timing plays a major role. Whether you’re building a data logger, controlling events, or creating a high-precision clock, how accurately you keep time can significantly impact your project.

In this guide, we’ll explore how to use the DS3231 Real-Time Clock (RTC) module with an Arduino RP2040 Connect to achieve precise timing. We’ll walk through the setup, explain the logic, and show you how to deal with the inevitable time drift that comes with long-term use.

Why Precision Timing Matters

Let’s start with why precision timing is so important. Imagine you’re working on a project that requires logging data at regular intervals, or you need to trigger events that must happen exactly on time. Any drift in the timing could lead to inaccuracies, which can mess up your entire project. This is where the DS3231 RTC module shines.

Getting Started: Setting Up the DS3231

First things first, let’s talk about setting up the DS3231 with your Arduino. The DS3231 is a simple yet powerful module that keeps track of time with incredible accuracy. You’ll connect it to your Arduino as below,

Circuit Diagram

Understanding the Timing Logic

The heart of this project is the logic that keeps time accurately and compensates for any drift over time.

Here’s how it works,

  1. Initializing the Time
  • When you start your Arduino, the DS3231 keeps track of the current time. We sync this time with the Arduino’s internal clock to ensure everything starts in sync.

2. Counting Time with the 32K Pin:

  • The DS3231 has a 32KHz pin that generates a pulse 32,768 times a second. By counting these pulses, we can keep track of time down to the millisecond. This pulse is super accurate, which helps keep your timing spot-on.

3. Handling Time Drift:

  • No clock is perfect. Over time, the Arduino’s clock and the DS3231 can drift apart, meaning the Arduino might think more or less time has passed than the DS3231 does. To handle this, we periodically check the temperature (since temperature changes can affect timing) and adjust the time to compensate.

4. Running Your Main Loop:

  • In your main loop, you’ll be checking if it’s time to perform an action (like turning an LED on or off). If it’s time, you do the action and then calculate when the next action should happen. This way, you keep everything running on schedule.

5. Storing and Analyzing Timing Data:

  • As your project runs, you might want to store timing data to see how well your system is performing. By comparing the DS3231 time with the Arduino’s time, you can calculate how accurate your timing is and make any necessary adjustments.

Dealing with Drift

Even the best clocks experience drift over time. This happens because factors like temperature can cause slight changes in the clock’s speed. To deal with this, we use the temperature data from the DS3231 to adjust the time. By tweaking the timing based on temperature changes, we can keep everything running smoothly.

Drift is usually small but can add up over time. That’s why it’s important to monitor it and make adjustments as needed. In our setup, we check for drift every minute. If we find that the Arduino’s clock is running faster or slower than the DS3231, we adjust the timing accordingly.

I will attach a code without the drift adjustment,

#include <DS3231.h>
#include <Wire.h>

#define MAX_EVENTS 10000 // Maximum number of events to store
#define Pin32K 4 // Pin connected to DS3231 32K output

// Structure to store timing events
struct TimingEvent {
long ds3231Time;
long arduinoTime;
};

TimingEvent events[MAX_EVENTS];
int eventIndex = 0;

DS3231 rtc;
RTClib myRTC;

// Calibration Parameters (Adjust these based on careful measurements)
float temp_offset = 0.0;
float ppm_per_degree = 2.0;

volatile long lastRTCTime = 0; // Stores the timestamp of the last interrupt
volatile uint32_t Pin32KCount = 0;
static unsigned long lastDriftCheck = 0;
const int driftCheckInterval = 60 * 1000; // Check every 1 minute
long long driftAccumulator = 0; // Accumulates drift over measurements

static long nextLoopTime = 0; // Stores the time the next loop should start
const long loopIntervalMillis = 21; // 21 ms for approximately 47 Hz

void setup() {
// Initialize Serial communication
Serial.begin(115200);

// Initialize I2C communication
Wire.begin();

Serial.println("Starting");

// Set up the 32K pin as input with pull-up resistor
pinMode(Pin32K, INPUT_PULLUP);
digitalWrite(Pin32K, HIGH);

// Enable the 32KHz output on the DS3231
rtc.enable32kHz(true);

// Attach interrupt to count pulses from the 32KHz pin
attachInterrupt(digitalPinToInterrupt(Pin32K), sqwCounter, FALLING);
}

void loop() {
// Initialize timing for the first loop iteration
if (nextLoopTime == 0) {
DateTime t = myRTC.now();
lastRTCTime = t.unixtime() * 1000; // Convert Unix time to milliseconds
nextLoopTime = lastRTCTime + loopIntervalMillis;
}

while (true) {
// Calculate elapsed time based on 32KHz pulses
uint32_t currentCount = Pin32KCount;
uint32_t elapsedMillis = (currentCount / 32.5);
unsigned long adjustedTime = lastRTCTime + elapsedMillis;

// If more than 1 second has passed, update the RTC time
if (elapsedMillis > 1000) {
DateTime t = myRTC.now();
float temp = rtc.getTemperature();
lastRTCTime = (t.unixtime() * 1000) - 1000;
Pin32KCount = 0;
}

// Check if it's time to execute the next loop iteration
if (adjustedTime >= nextLoopTime) {
nextLoopTime += loopIntervalMillis;

// Store timing data
events[eventIndex].ds3231Time = adjustedTime;
events[eventIndex].arduinoTime = millis();
eventIndex = (eventIndex + 1) % MAX_EVENTS; // Circular buffer

// Print timing event data to Serial Monitor
String eventString = "";
eventString += String(adjustedTime);
eventString += ".timing_event."; // Custom event label
eventString += String(millis());
eventString += ",";
Serial.println(eventString);

// Check for drift and compensate if necessary
if (millis() - lastDriftCheck >= driftCheckInterval) {
long currentTime = adjustedTime;
long arduinoElapsedMillis = millis() - lastDriftCheck;
lastDriftCheck = millis();
long rtcElapsedMillis = currentTime - driftAccumulator;
driftAccumulator = currentTime;

long drift = rtcElapsedMillis - arduinoElapsedMillis;
long driftPerHour = (drift * 3600 * 1000) / driftCheckInterval;

// Print drift information to Serial Monitor (optional)
Serial.print("Drift (millis) in the last minute: ");
Serial.println(drift);
Serial.print("Drift per hour: ");
Serial.println(driftPerHour);
}
break;
}
}
}

// Interrupt service routine to count pulses from the 32KHz output
void sqwCounter(void) {
Pin32KCount++;
}

// Function to calculate RMSE (Root Mean Square Error)
float calculateRMSE(TimingEvent *data, int windowSize) {
long sumSqErrors = 0;
int count = 0;

for (int i = eventIndex - windowSize; i < eventIndex; i++) {
if (i >= 0) { // Handle the beginning of the array
long diff = data[i].ds3231Time - data[i].arduinoTime;
sumSqErrors += diff * diff;
count++;
}
}

if (count == 0) {
return 0.0; // Return 0 if no data is available
}

float meanSqError = sumSqErrors / count;
return sqrt(meanSqError);
}

Output

Final Thoughts

Precision timing might seem like a small detail, but it’s critical in most of the embedded systems projects. With the DS3231 RTC module, you can achieve the accuracy you need without breaking a sweat. By following the steps we’ve outlined, you’ll have the best timing system that compensates for drift and keeps everything running like clockwork.

So, go ahead and try it out! Once you see how well it works, you’ll wonder how you ever got by without it. And as always, if you run into any issues or have questions, feel free to reach out.

Hope you enjoyed the article. Please comment below or send us an email to info@protonest.co, if you face any issues when implementing.

Contact us for any consultations or projects related to embedded systems.

Email: info@protonest.co

Protonest for more details.

Protonest specializes in transforming IoT ideas into reality. We offer prototyping services from concept to completion. Our commitment ensures that your visionary IoT concepts become tangible, innovative, and advanced prototypes.

Our Website: https://www.protonest.co/

Cheers!

--

--

Protonest IoT
Protonest IoT

Written by Protonest IoT

We make your IoT ideas a reality

No responses yet