I'm still waiting for my DS3231Ms from the Far East, but have finished the software I'll be using to calibrate them. It works on the SN module I already have, so I think it will also work for the Ms.
I was looking for a way to sync the RTC to my computer's time. And it turns out that VBScript in Windows has a SendKeys function that can be used to stuff the system timestamp into the keyboard input buffer of a running application - the Serial Monitor in this case. So I have a short script that first waits for the current second to change, then dumps the timestamp to the Serial Monitor, and the Arduino in turn writes that info to the RTC. It all happens very fast.
' 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
You would want to first sync the computer to some reliable NTP server. Then the Aduino sketch boots up and displays the current RTC timestamp, followed by 12 seconds of time ticks which can be compared with the PC's clock to see how out of sync they are. Then executing Timestamp.vbs will sync the RTC to the computer.
You can also display the current Aging register setting, or change it to a new value. The idea is that tweaking the Aging register over time could get the RTC as close as possible to perfect time. If you can get it to one second a week, that's 1.6ppm or about a minute a year, which is pretty good. But you can detect out-of-sync by as little as about 0.1 second, and the SN datasheet says changing Aging by 1 produces a 0.1ppm change, so in theory you could get it to be quite accurate.
Below is the Arduino sketch for an Uno or Nano. No library required except Wire.h. I'm hoping someone here will be able to play with it and post what you think.
/*
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;
}