Here's the sketch. You'll need the Serial Monitor open. No libraries needed except Wire.
/*
DS3231 Aging from GPS
This sketch compares the SQW output of a DS3231 relative to the PPS output of a GPS module,
and adjusts the RTC's Aging register so the RTC clock is running at the same speed as GPS.
Comparisons are done every five minutes. Results are valid only at the current temperature.
This runs on an ATMega328P Arduino (Uno, Nano, Pro Mini) with the PPS line of a GPS module
tied to D8, and the SQW output of a DS3231 RTC tied to D2. VCC, GND, SDA and SCL as usual.
After bootup, wait for the GPS to begin one-second flashing, then press any key to begin.
This sketch will change the RTC time by up to one second. It does not set the correct time
per GPS - it only optimizes the Aging register setting. When Aging is where you want it,
enter 'Q' to quit, or 'T' to set the RTC to a new date and time you will enter, then quit.
Enter 'An' at any time to set Aging to n, then start over (-128 to 127 permitted).
*/
#include <Wire.h>
#define flagsREG EIFR // ATMega328P interrupt flags register
const byte RTCpin = 2; // D2
volatile bool Capture = false; // Capture interrupt has occurred - GPS
volatile bool Started = false; // Square wave interrupt has occurred - RTC
volatile byte MSBtimer; // MS byte of 24-bit timer
volatile int Square = 0; // square wave clocks - for SN vs M
bool isSN = true, Pending = false; // true if SN, false if M; forced conv pending
bool fineFlag = false; // switch to max deltaAging of +/- 1
unsigned long GPTlow, GPThi; // timer counts on GPS interrupt
long Diff, prevDiff, oldDiff, deltaDiff; // clocks from RTC to GPS
int ppm; // Aging +/- 1: count difference over 5 minutes
int SNdivisor = 264; // ppm for SN parts
int Mdivisor = 576; // ppm for M parts
int Restart = 32; // initial 32-sec run-in
int Period = 300; // number of seconds between adjustments
int finePeriod = 200; // period when close to end
int Count = Restart, k; // down counter - seconds until next calc
int8_t Aging, deltaAging; // contents of RTC Aging register
byte Seconds, Control, Status, j, i = 0; // other RTC registers' contents
long batch[16]; // last 16 readings
char buff[20]; // serial input buffer
byte buffSize; // length of input string
char in; // serial input character
void setup() {
Serial.begin(57600);
delay(2000);
Wire.begin();
pinMode(8,INPUT); // Timer1 capture input - from GPS (ICP1)
pinMode(RTCpin,INPUT_PULLUP); // Hardware interrupt on D2 - from RTC
Serial.println("Enter any key to begin"); // Wait for GPS to begin 1-sec flashes
Serial.println();
int key1 = 0;
while ((key1 != 10) && (key1 != 13)) {
key1 = Serial.read();
delay (100);
}
key1 = Serial.read(); // in case CR/LF
// Clear /EOSC, CONV, RS2, INTCN, A2E, A1E. Set BBSQW, RS1. (SQW freq = 1KHz)
Control = 0b01001000;
Status = 0;
updateReg(0x0E); // update Control
updateReg(0x0F); // update Status
Wire.beginTransmission(0x68); // read Aging and Seconds registers
Wire.write(0x10);
Wire.endTransmission();
Wire.requestFrom(0x68, 4);
Aging = Wire.read(); // starting value of the Aging register
Seconds = Wire.read(); // read past temp registers
Seconds = Wire.read();
Seconds = Wire.read(); // pointer wraps to zero
cli();
flagsREG = 3; // clear any flags on both pins
attachInterrupt(digitalPinToInterrupt(RTCpin),SNvsM, FALLING);
flagsREG = 3;
sei();
delay(2000);
Control &= 0b11110111; // change squarewave to 1Hz
updateReg(0x0E);
detachInterrupt(digitalPinToInterrupt(RTCpin)); // will assign new ISR for D2
flagsREG = 3;
Serial.print ("1KHz squarewave makes "); Serial.print(Square);
Serial.println (" cycles over 2 seconds.");
if (Square < 500) {
isSN = false;
ppm = Mdivisor; // expected change from Aging +/- 1
Serial.println ("So this is a DS3231M");
}
else {
ppm = SNdivisor; // expected change from Aging +/- 1
Serial.println ("So this is a DS3231SN");
}
Serial.println();
if (F_CPU == 8000000) ppm /= 2; // if 8MHz Pro Mini
while(digitalRead(8)); // wait for GPS low, then
while(!digitalRead(8)); // wait for GPS high - beginning of second
delay(500); // wait half a second
updateReg(0); // reinitialize RTC clock - now 1/2 sec apart
cli();
TIMSK0 = 0; // Disable Timer0 interrupts (millis)
TCCR0A = 0;
TCCR0B = 0;
TIFR0 = 0xFF;
TCCR1A = 0; // set up Timer1
TCCR1B = 0;
TCCR1C = 0;
TCNT0 = 0; // clear Timer1
TIFR1 = 0xFF; // clear flags
TIMSK1 = 0b00100001; // enable capture and overflow interrupt (GPS)
TIFR1 = 0xFF; // clear flags
TCCR1A = 0b00000000; // Normal mode, no output, WGM #0
TCCR1B = 0b01000001; // rising edge capture, timer1 on, no prescale
flagsREG = 3; // new ISR for D2
attachInterrupt(digitalPinToInterrupt(RTCpin),rtcISR, FALLING);
flagsREG = 3;
sei();
Serial.println ("Enter 'An' to change Aging to n (-128 to 127)");
Serial.println ("Enter 'Q' to quit, or 'T' to enter new date/time"); Serial.println();
}
void loop() {
if (Capture) { // GPS PPS has gone high
Capture = false;
GPTlow = ICR1; // read timer values
GPThi = MSBtimer;
cli();
TCNT1 = 0; // clear timer1
MSBtimer = 0;
TIFR1 = 0xFF; // clear flags
sei();
Diff = (GPThi << 16) + GPTlow; // combine timer counts to one long value
if (abs(Diff - prevDiff) < 1000) { // normal values only
batch[i] = Diff; // collect last 16 values into array
i = (i + 1) & 15;
}
prevDiff = Diff;
Count--;
if (!Count) { // do calculation every five minutes
Diff = 0;
for (j = 0; j < 16; j++) {
Diff += batch[j];
}
Diff = (Diff + 8) / 16; // average over last 16 seconds
if (Restart == 32) oldDiff = Diff;
deltaDiff = Diff - oldDiff; // calculate new Aging
deltaAging = deltaDiff / ppm;
if ((Restart != 32) && (!deltaAging)) fineFlag = true;
if (deltaAging) { // if any change
if (fineFlag && (deltaAging > 1)) deltaAging = 1;
if (fineFlag && (deltaAging < -1)) deltaAging = -1;
Aging += deltaAging;
updateReg(0x10);
oldDiff = Diff;
if (isSN) Pending = true; // force conversion if SN
}
Serial.print ("Diff "); Serial.println(Diff); // print results
Serial.print ("deltaDiff ");
if (deltaAging == 0) {
Serial.print("[");
Serial.print(deltaDiff);
Serial.println("]");
}
else Serial.println(deltaDiff);
Serial.print ("deltaAging "); Serial.println(deltaAging);
Serial.print ("Aging "); Serial.println(Aging); Serial.println();
if (Restart==32) Restart = Period; // switch to 5 minutes after run-in
if (fineFlag) Restart = finePeriod; // switch to 3.33 minutes in fine mode
Count = Restart;
}
}
if (Started) { // beginning of second
if (Pending) {
Control |= 0b00100000; // force conversion
updateReg(0x0E);
Control &= 0b11011111;
Pending = false;
}
Started = false;
}
if(Serial.available()) { // process input from Serial Monitor
in = Serial.read(); // set end-line option to Newline or CR
if ((in == 13) || (in == 10)) {
buff[buffSize] = 0;
parse_cmd(buff, buffSize);
buffSize = 0;
buff[0] = 0;
}
else {
buff[buffSize] = in;
buffSize++;
}
}
}
void parse_cmd(char *cmd, byte cmdsize) {
// YYYYMMDDWhhmmss
if ((cmd[0]=='2')&&(cmdsize==15)) { // "2" new date/time
Wire.beginTransmission(0x68);
Wire.write(0);
Wire.write(inp2bcd(cmd,13)); // seconds
Wire.write(inp2bcd(cmd,11)); // minutes
Wire.write(inp2bcd(cmd,9)); // hours
Wire.write(cmd[8] - 48); // day of the week
Wire.write(inp2bcd(cmd,6)); // date of the month
Wire.write(inp2bcd(cmd,4) | 0x80); // month & century
Wire.write(inp2bcd(cmd,2)); // year
Wire.endTransmission();
Serial.println ("Data entered");
shutdown();
}
else if ((cmd[0]&0xDF)=='T') { // "T" Time set
Serial.println ("Enter new date/time for RTC. (w = day of week (1-7))");
Serial.println ("YYYYMMDDwhhmmss");
}
else if ((cmd[0]&0xDF)=='A') { // "A" Aging
if (cmdsize > 1) {
k = atoi(&cmd[1]); // get value of string
if ((k < 128) && (k > -129)) { // check for legit value
Aging = k; // convert to signed byte
updateReg(0x10); // write to Aging register
Count = 32; Restart = 32; fineFlag = false;
if (isSN) Pending = true;
}
else Serial.println ("Invalid Aging Value");
}
Wire.beginTransmission(0x68); // "A" alone prints current value
Wire.write(0x10);
Wire.endTransmission();
Wire.requestFrom(0x68, 1);
Aging = Wire.read();
Serial.print("Aging = "); Serial.println(Aging); Serial.println();
}
else if ((cmd[0]&0xDF)=='Q') { // "Q" Quit
shutdown();
}
}
byte inp2bcd(char *inp, byte seek) {
return (((inp[seek]-48)<<4) + (inp[seek+1] - 48));
}
void shutdown() {
Control |= 0b00000100; // disable square wave
updateReg(0x0E);
cli();
detachInterrupt(digitalPinToInterrupt(RTCpin));
flagsREG = 3;
TCCR1B = 0;
TIMSK1 = 0;
TIFR1 = 0;
sei();
Serial.println("Squarewave disabled");
Serial.println("Shutting down");
while (1);
}
void updateReg(byte addr) {
Wire.beginTransmission(0x68);
Wire.write(addr);
if(addr == 0x0E) Wire.write(Control);
else if(addr == 0x0F) Wire.write(Status);
else if(addr == 0x10) Wire.write(Aging);
else if(addr == 0) Wire.write(Seconds);
Wire.endTransmission();
}
ISR(TIMER1_CAPT_vect) {
TCCR1B &= 0xFE; // stop Timer1 clock
Capture = true;
}
ISR(TIMER1_OVF_vect) {
MSBtimer++; // increment MSB on overflow
}
void rtcISR() {
TCCR1B |= 1; // start Timer1 clock
Started = true;
}
void SNvsM() { // only used for SN vs M test
Square++;
}