DS3231 - setting Aging register to optimum value

This started when I wanted a way to sync the RTC's time to my PC's time after the PC has been synced to NTP. I found that VBScript has a SendKeys function that will stuff the current timestamp into another program's keyboard buffer, which in this case is the IDE's Serial Monitor. Then the Arduino sketch parses that input and sets the RTC time accordingly. I'll post the VBS script and Arduino sketch below in case anyone is interested.

But then it occurred to me that the same method could be used to tweak the Aging register to the value that makes the RTC most accurate. The script would send the timestamp at the beginning of the PC's second, and the RTC will generate an interrupt at the beginning of its second. With the two seconds values and the millis() values when each occurs, that would give a pretty accurate differential which could be used to adjust Aging. Doing this every 15 minutes, it might even be possible to use some kind of successive approximation to get pretty quickly to the optimum point.

However, I wonder if NTP is consistent enough for this to work. As I understand it, NTP assumes that half of the total round trip delay occurs on the return packet, and I don't see any reason why that would be the case. If the actual return packet delay varies a lot from the theoretical, then that would limit the usefulness of my method. Does anyone have experience with this aspect of NTP that could advise me? An alternative would possibly be a WWVB receiver, but there again propagation delays could vary a lot.

The idea is to get the DS3231 as accurate as possible before deploying it into the field where there will be no communications available to keep the time correct. Well, I guess other than WWVB, if that actually works, and doesn't use to much power.

' Timestamp.vbs
' VBScript for Windows

' This script sends the current system date/time to the keyboard input of the
' Arduino Serial Monitor. If more than one Serial Monitor is open, change the
' "COM" entry below to the specific COM port used for the RTC sketch (i.e. "COM3").

' The Serial Monitor line-end setting must be set to Newline or Carriage Return.

' The "Weekday(dDate,1)" entry is for Sunday being day 1.
' Change to "Weekday(dDate,2)" to make Monday day 1.


Set WshShell = WScript.CreateObject("WScript.Shell")

WshShell.AppActivate "COM"

oldTime = Time
While oldTime = Time
Wend

WshShell.SendKeys TimeStamp(Now)

Function TimeStamp(dDate)
    TimeStamp = "S"&right("0"&second(dDate),2)&"{ENTER}"&"X"&right("0"&Minute(dDate),2) _
	&right("0"&Hour(dDate),2)&Weekday(dDate,1)&right("0"&Day(dDate),2) _
	&right("0"&Month(dDate),2)&right(Year(dDate),2)&"{ENTER}"
End Function
/*
DS3231TimeCheck.ino

Connect the DS3231 RTC to an Uno or Nano, with the INT/SQW
pin connected to D2.  On boot the RTC's time stamp is
displayed on the Serial Monitor, followed by a 12-second
count, each tick triggered by an RTC interrupt, which can
be compared to the PC's clock.

Execute the script Timestamp.vbs to transmit the Windows
PC's time to the RTC.  The PC should have first been synced
to an NTP server.

Type "T" to display another 12 RTC clock ticks.

Type "A" to display the current value of the Aging register,
or "An", to assign the value of n to the Aging register,
which must be between -128 and +127.
*/

#include <Wire.h>
#define flagsREG EIFR                     // Atmega328P flags register

byte Seconds, Min, Control, Status, Count, buff_size;
int8_t Aging;                             // signed byte
char buff[40];
byte r[13];                               // registers read
const byte aPIN = 2;                      // D2
const byte ticks = 12;
volatile bool Alarm = false;
void setup() {

  Serial.begin(57600);                    // all development was done at this speed
  delay(2000);
  Wire.begin();
  delay(10);
  pinMode(aPIN, INPUT_PULLUP);

  Wire.beginTransmission(0x68);           // read Control and Status registers
  Wire.write(0x0E);                       //   and clear alarm enables and flags
  Wire.endTransmission();
  Wire.requestFrom(0x68, 2);

     // Clear /EOSC, A2E, AE1. Set BBSQW, INTCN
  Control = (Wire.read() & 0b01111100) | 0b01000100;

     // Clear OSF, EN32k, A2F, A1F
  Status = Wire.read() & 0b01110100;

  updateReg(0x0E);                        // update Control
  updateReg(0x0F);                        // update Status

  Wire.beginTransmission(0x68);           // address of DS3231
  Wire.write(7);                          // select register = Alarm1 seconds
  Wire.write(0x80);                       // alarm on each second
  Wire.write(0x80);
  Wire.write(0x80);
  Wire.write(0x80);
  Wire.endTransmission();

  noInterrupts();
  flagsREG = 3;                           // clear any flags on both pins
  attachInterrupt(digitalPinToInterrupt(aPIN),rtcISR, FALLING);
  flagsREG = 3;
  interrupts();

  Startup();
  buff_size = 0;
  buff[0] = 0;
}

void loop() {

  if(Alarm) {
    Seconds++;
    if (Seconds == 60) Seconds = 0;
    if(Seconds == 0) {
      Wire.beginTransmission(0x68);       // address DS3231
      Wire.write(1);                      // minutes register
      Wire.endTransmission();
      Wire.requestFrom(0x68, 1);
      Min = bcd2dec(Wire.read());
      Serial.print(Min);Serial.print(":");
    }
    Serial.println(Seconds);

    Alarm = false;
    updateReg(0x0F);                      // clear alarm1 flag

    Count--;
    if (Count == 0) {
      Control &= 0xFE;                    // disable alarms
      updateReg(0x0E);
    }
  }

  if(Serial.available()) {                // process input from Serial Monitor
    char in = Serial.read();              // set end-line option to Newline or CR
    if ((in == 13) || (in == 10)) {
      buff[buff_size] = 0;
      parse_cmd(buff, buff_size);
      buff_size = 0;
      buff[0] = 0;
    }
    else {
      buff[buff_size] = in;
      buff_size += 1;
    }
  }
}

void parse_cmd(char *cmd, byte cmdsize) {
  // Sss  seconds                         // "S" seconds
  if ((cmd[0] == 83) && (cmdsize == 3)) {
    Wire.beginTransmission(0x68);
    Wire.write(0);
    Wire.write(inp2bcd(cmd,1));
    Wire.endTransmission();
  }
  // XmmhhWDDMMYY rest of timestamp
  else if((cmd[0]==88)&&(cmdsize==12)) {  // "X" rest of timestamp
    Wire.beginTransmission(0x68);
    Wire.write(1);
    Wire.write(inp2bcd(cmd,1));           // minutes
    Wire.write(inp2bcd(cmd,3));           // hours
    Wire.write(cmd[5] - 48);              // day of the week
    Wire.write(inp2bcd(cmd,6));           // date of the month
    Wire.write(inp2bcd(cmd,8) | 0x80);    // month & century
    Wire.write(inp2bcd(cmd,10));          // short year
    Wire.endTransmission();
    RTCstamp();
    updateReg(0x0F);                      // clear alarm flags
    Control |= 1;
    updateReg(0x0E);                      // enable alarm1
  }
  else if ((cmd[0]==65)||(cmd[0]==97)){   // "A" Aging
    if (cmdsize > 1) {
      int 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
      }
      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);
  } 
  else if ((cmd[0]==84)||(cmd[0]==116)) { // "T" enable ticks
    Startup();
  }
}

void Startup() {
  updateReg(0x0F);                        // clear alarm flags
  Control |= 1;
  updateReg(0x0E);                        // enable alarm1
  while (!Alarm);
  Alarm = false;
  updateReg(0x0F);                        // clear alarm flags
  RTCstamp();        
}

void RTCstamp() {                         // print current RTC timestamp
  Wire.beginTransmission(0x68);
  Wire.write(0);
  Wire.endTransmission();
  Wire.requestFrom(0x68, 7);
  for (byte i = 0; i<7; i++) {
    r[i] = bcd2dec(Wire.read());
  }
  snprintf(buff,40,"%d/%02d/%02d Day%1d %02d:%02d:%02d",r[6]+2000,r[5],r[4],r[3],r[2],r[1],r[0]);
  Serial.println(buff);
  Seconds = r[0];
  Count = ticks;
}

byte bcd2dec(byte n){
  n &= 0x7F;                              // mask out Century bit
  return n - 6 * (n >> 4);
}

byte dec2bcd(byte n){
  return ((n / 10 * 16) + (n % 10));
}

byte inp2bcd(char *inp, byte seek) {
  return (((inp[seek]-48)<<4) + (inp[seek+1] - 48));
}

void updateReg(byte addr) {
  Wire.beginTransmission(0x68);
  Wire.write(addr);
  if(addr == 0x0E) Wire.write(Control);        // enable alarm1
  else if(addr == 0x0F) Wire.write(Status);    // clear alarm flags
  else if(addr == 0x10) Wire.write(Aging);     // update Aging register
  Wire.endTransmission();
}

void rtcISR() {
Alarm = true;
}

If you want to use NTP for accurate interval measurement, you need to use extremely long measurement intervals because of the timing skew caused by variable network delays.

Even when using GPS PPS to do this job, I found that a good calibration could not be done in less than a few minutes (5-8 IIRC). That is with a PPS timing spec of about +/- 10us. The resulting calibration was good to about 0.1 PPM.

I was doing it with the STM32 internal RTC, but the same principles would apply to the DS3231 or any RTC with a variable reference.

As I understand it, NTP would be tens of milliseconds, not microseconds. So maybe 15 minutes wouldn't be long enough. Do you have any experience with WWVB? The Chinese sell demodulators for just a few dollars.

Well, I may give NTP a try and see how it goes.

No experience, except I once had a WWVB based wall clock. If you expect to do it quickly, GPS PPS is pretty well the only way. Any reference will work, given enough time. It's all about how quickly you can determine the DUT clock frequency.

You could condition a local, stabilized clock that you keep synced to NTP. Then measure with that.

I got it all working, but got confusing results.

After an update of the RTC time from the resynced PC time, the RTC will be a bit behind because of the time it takes to transmit to the Adruino and RTC. But if the Aging register is perfect, I should be able to resync the PC with NTP after 15 minutes, and send the seconds value to the Arduino, and the measured time differential should be the same as it originally was. I was able to get to that state over several periods, to the point that with Aging set to -14, I got exactly the same delay to the nearest millisecond.

But then I reset the RTC to NTP via the resynced PC in the usual manner, with Aging set to -14, but overnight it lost a noticeable fraction of a second - maybe 1/4 second - versus a freshly synced PC. So I don't know what's going on. I don't know if RTC timekeeping changes depending on what it's doing (generating the 1sec interrupt, doing I2C, or whatever) or depending on whether it's powered from Vcc or the coin cell, or what.

I guess I also don't know whether a resync of the PC actually changes the PC system time immediately, or whether it averages in any change over time.

The other thing is that in version 2 of the IDE, the serial monitor is no longer a separate process, so the VBS script might not be able to stuff the keyboard buffer properly. That can be solved by using a terminal app, but that just adds another level of complication.

So maybe this is going the require an ESP thing to do its own NTP. But I'm still puzzled by what appears to be a change in oscillator frequency caused by some unknown factor.

There are too many weak links in your timing chain, first Windows syncs up, then there is latency in the Windows app, then there is serial delay, etc. etc.

The overnight difference may be caused by a different supply voltage if you are calibrating under 5V power, and then going to standby on a 3V battery. The chip is designed for minimal effect from a supply voltage change, but it's not perfect.

I did the Arduino stuff at 3.3V using a Pro Mini. So there shouldn't be much difference vs a fresh coin cell. But that's certainly something I can test. But even with an ESP32, there's still going to be some uncertainty on network delays.

I wonder how much variation there is in propagation time for WWVB. I'm 570 miles from Boulder, which I think means there wouldn't be any bounce. I can get a receiver for $7.

Edit: Here's another thread suggesting I2C activity changes the oscillator speed. I should be able to test that too.

https://forum.arduino.cc/t/ds3231-timing-issues-under-frequent-i2c-reads/595337

None of my sketches would need to worry about that because I only ever check the RTC at 100ms intervals.

Do you have a link to the WWVB receiver? I'm curious. Sounds like a fun thing to play with.

I wonder how much variation there is in propagation time for WWVB

Much less than the encoding/decoding skew due to limited bandwidth.

Just search Ebay for WWVB receiver. There are dozens of them, all apparently the same thing. In any case, I forgot that there's a huge office tower less than a mile from me that is directly in the way. So I suspect WWVB is not going to be an option for me.

Right you are, I see them and I'm ordering one. I had a WWVB clock years ago, it was fussy about setting itself. I had to move it around the house before I found a spot where it would sync. I think that building won't be a problem for you, WWVB frequency is quite low, and gets around obstacles pretty well. Now that the solar cycle is on the upswing, the long distance propagation is improving, it should be viable.

Well I ordered one too. Would it be a fair assumption that the ferrite rod antenna's long axis would be at a right angle to Ft. Collins? In other words, the antenna is oriented broadside to the transmitter azimuth. Does it matter whether it is horizontal or vertical?

Yes, it seemed to matter more which way the clock was facing, than where in the house it was located. EM waves are transverse, so longitudinal orientation of the ferrite with respect to the transmitter would be incorrect. You are trying to pick up the H (magnetic) vector, the only question is whether it is vertical or horizontal. I don't know the Ft. Collins transmit polarization, so I can't guess on which way would work better.

When I get a hold of it, I'll have a view of the signal that I didn't have with the clock, so I may learn more...

I opened a new thread about using a WWVB receiver to calibrate the Aging register:

https://forum.arduino.cc/t/wwvb-decoder-software/1116490

but further into that thread switched over to discussing using the PPS signal of a GPS module instead. Since I've ended up using the GPS for this, not WWVB, I'd like to bring the discussion back here to present what I've developed for GPS so far, and ask about something that's a bit puzzling.

So to bring things up to date, I bought the GPS module and a 28db antenna from Amazon:

https://www.amazon.com/dp/B07P8YMVNT

https://www.amazon.com/dp/B07DMXGW5J

I got the "big" antenna because a number of reviews of the GPS module complained that the included antenna was no good. In any case,with the big antenna I get good reception indoors on the second floor, which is what I wanted. When first booted up the module takes up to 30 seconds to lock in enough satellites to begin the PPS signal, which goes high for 100ms once every second. I've also looked at the serial data, and it's pretty much error-free. So whoever made the module, it seems to work well, at least with the 28db antenna.

On the RTC side I have one module marked DS3231SN that turns out to be a real DS3231SN. And I have another one marked DS3231M which is indeed that. Then I have a third marked DS3231SN that is in fact a DS3231M. No, that's not right. It's not even a real M because Maxim would never have done that. So its just a complete fake that seems to work like an M.

Anyway, the idea is to find the Aging setting that makes the RTC clock run at the same rate as the GPS. I enable the RTC's 1Hz squarewave output, about 1/2 second earlier than the PPS. Then I measure the number of 16MHz CPU cycles that take place between the two, which of course is about 8 million. Then looking at two such readings taken 5 minutes apart, I can tell whether the RTC is gaining or losing time relative to GPS, and by how much. I divide the difference by what the datasheet says is the effect over 5 minutes of a +/- 1 change to the Aging value, and change Aging accordingly. The first adjustment gets it very close to what ends up being the final value, but I can run it for a number of 5-minute cycles to be sure. Of course it's never going to be perfect because one value will run just a little fast, but +1 will be just a little slow. So you just get as close as you can, and pick one.

I run Timer1 in normal mode at full speed (no prescaler), and accumulate overflow interrupts in a variable. That's in effect a 24-bit timer. After a GPS triggered capture and interrupt, I take the timer readings, then stop the timer and clear its count and the variable count. Then when the RTC squarewave interrupts, it just start the timer. The millis interrupt on Timer0 is disabled so it won't interfere.

I'll post the sketch in the next post. If anyone has a GPS with a PPS output, and any DS3231 modules, I hope you will experiment and report what results you get.

The thing that I'm puzzled about is the datasheet spec for how much a change of the LS bit of the Aging register changes the internal clock. The M datasheet says this is .12 ppm at 25C, which is 576 cycles over 5 minutes at 16MHz. Indeed, when I get the comparison from the first two readings 5 minutes apart, I can divide the difference by 576, and I get a new aging value that is very close to the final optimal value - usually +/- 1.

The datasheet for the SN says it's .10 ppm, which would be 480 cycles. But on my one genuine SN example, it turns out to be .055 ppm. or about 264 cycles. That's the divisor that gets me close to the final answer on the first comparison. But I don't know if that's true just for my SN, or true for all of them. Actually, it would be good if the lower number is valid for all because it means you can get that much closer to perfect time. If the most you could be fast or slow is .055 ppm, that would be 1.7 seconds per year all else being equal.

I had to deliberately set the Aging value of the SN off a good bit (+50) to test the divisor. That's because Aging was at zero by default, and the final optimal value was +1. But the Ms were different. Also zero by default, their optimums were -44 and -19, so off a good bit. So it may turn out that only Ms will benefit materially from this Aging optimization.

In case anyone is interested, my genuine SN is one I recently bought, which is surprising. It comes on a circular board, with no EEPROM, no"charging" circuit, no LED and no pullup resistors (but holes for the SDA and SCL pullups). But unfortunately the battery holder is for a CR1220, which has about 1/6 the capacity of a CR2032.

https://www.ebay.com/itm/401482226870?var=671154997502

I'll put the code in the next post.

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++;
}

And here is the output stream from a sample run on what I think is a legit DS3231M. The new Diff value becomes the old Diff value only when Aging is actually changed. So the deltaDiff values that appear in [brackets] are just interim cumulative values pending a change to Aging.

Enter any key to begin

1KHz squarewave makes 2 cycles over 2 seconds.
So this is a DS3231M

Enter 'An' to change Aging to n (-128 to 127)
Enter 'Q' to quit, or 'T' to enter new date/time

Diff 8021553
deltaDiff [0]
deltaAging 0
Aging 0

Diff 8009888
deltaDiff -11665
deltaAging -20
Aging -20

Diff 8008997
deltaDiff -891
deltaAging -1
Aging -21

Diff 8009969
deltaDiff 972
deltaAging 1
Aging -20

Diff 8010346
deltaDiff [377]
deltaAging 0
Aging -20

Diff 8010465
deltaDiff [496]
deltaAging 0
Aging -20

Diff 8010848
deltaDiff 879
deltaAging 1
Aging -19

Diff 8010873
deltaDiff [25]
deltaAging 0
Aging -19

Diff 8010872
deltaDiff [24]
deltaAging 0
Aging -19

Diff 8010657
deltaDiff [-191]
deltaAging 0
Aging -19

Diff 8010378
deltaDiff [-470]
deltaAging 0
Aging -19

Diff 8009768
deltaDiff -1080
deltaAging -1
Aging -20

Diff 8010196
deltaDiff [428]
deltaAging 0
Aging -20

Diff 8010441
deltaDiff 673
deltaAging 1
Aging -19

Squarewave disabled
Shutting down

Interesting, thanks. I haven't received my unit in the mail yet, though, to test...

I'm not sure you can rely on perfect linearity in the capacitor bank that is used internally to implement the adjustments. Or, the exaqt step size. For example, the DS3234 (sorry I couldn't find the 3231 sheet immediately) sheet adds:

The change in ppm per LSB is different at different temperatures.

Yes, that's the part I'm still thinking about. I could read the temperature registers, and change the divisor based on that, but there are two diagrams in the datasheet that deal with that, and I don't understand either of them.

But it appears to be pretty linear at room temperature. In other words, the same divisor works no matter how far off the initial Aging setting is.

I just wish I could receive GPS from inside my refrigerator. But perhaps I can test the warm side with a space heater.

I wanted to report that the optimum setting of the aging register on DS3231 RTCs continues to change as the parts get older. Well, I guess that's what the register is for.

Over two months my new genuine DS3231SN part has gone from an optimum setting of +1 in the beginning to -5 now. My new genuine DS3231M has gone from -19 to -24. And my years-old fake SN, which behaves like an M, has gone from -44 to -46.

I still haven't done any work on how temperature changes affect all this, but I think it's clear that the M parts in particular may be very slow coming from the factory, and need to be aging calibrated. However, it appears that once calibrated they can do pretty well. I think the SN parts are clearly better, but they are becoming very hard to find.