The DS3231 is one of the most popular real-time clocks for Arduino projects. It is inexpensive, accurate and easy to use. In many applications it plays an important role—or could do so just as well. Whether controlling timers, switching relays, recording events or adding timestamps, the software required for normal operation is usually straightforward.
The difficult part is not using the RTC. The difficult part is setting it up.
A newly installed RTC has no meaningful date or time. Somehow these values have to be entered. That often means adding user interface code to the application sketch: buttons, displays, serial commands, Bluetooth, Wi-Fi, Ethernet or other temporary setup mechanisms. Even if the RTC only has to be configured once, the application ends up carrying code that is rarely, if ever, used again.
A common shortcut is to initialize the RTC with the compilation date and time (__DATE__ and __TIME__). While simple, this does not allow the RTC to be set accurately.
This article follows a different approach.
Instead of embedding RTC setup code into every application sketch, these infrequent tasks are moved into a dedicated DS3231 Service Tool. Date and time are configured only when required, while the application sketch remains focused on its actual purpose.
But this is only the first advantage.
The same service tool can also determine the appropriate DS3231 Aging Offset from a measured long-term drift and write it directly to the RTC. Once the appropriate Aging Offset has been determined and written, the RTC can become accurate enough that even these occasional service operations become much less frequent.
Imagine an Arduino that simply switches a mains socket on and off according to the time of day. Once the RTC has been set and even calibrated, why should the application sketch still contain many lines of code for tasks that may never be needed again?
Requirements
The reference implementation was developed and tested with the following hardware:
- an Arduino Uno R3
- a 5 V DS3231 RTC module
- a USB cable for sketch upload and Serial Monitor access
- four jumper wires
No display, buttons or additional communication hardware are required. The complete user interface is provided through the Arduino Serial Monitor.
Other Arduino boards can also be used, provided that their supply voltage and I²C logic levels are compatible with the RTC module. If the module is designed for 3.3 V operation only, power it accordingly and ensure that the I²C interface is electrically compatible.
To retain the date, time and Aging Offset while the main power is removed, the RTC must be fitted with a working backup battery.
Important: Some DS3231 modules include a charging circuit intended for a rechargeable backup battery. If your module includes such a circuit, either use a rechargeable battery or disable the charging circuit before installing a non-rechargeable battery. Instructions for identifying and modifying these modules are readily available on the Internet.
Typical use cases
1. Preparing an RTC for immediate use
This is the simplest use case.
- Install a backup battery.
- Use the DS3231 Service Tool.
- Set the correct date and time using a trusted time reference.
- Install the RTC in its project.
2. Preparing and optimizing a new RTC
If long-term accuracy is important, the RTC can be calibrated before it is installed in its final project.
- Install a backup battery.
- Use the DS3231 Service Tool.
- Set the correct date and time using a trusted time reference.
- Store the RTC for weeks or months.
- Use the DS3231 Service Tool again.
- Read the current RTC time.
- Compare it with the same trusted time reference and note the deviation in seconds together with the corresponding measurement period in days.
- Calculate and write the Aging Offset.
- Optionally repeat the procedure after another few weeks or months to verify the improvement.
- Install the calibrated RTC in its final project.
3. Calibrating an RTC already in service
An RTC that has already been operating inside a project can be (re)calibrated in exactly the same way.
- Upload the DS3231 Service Tool.
- Set the correct date and time using a trusted time reference.
- Upload the application sketch and continue normal operation for weeks or months.
- Upload the DS3231 Service Tool again.
- Compare the RTC time with the same trusted time reference and note the deviation in seconds together with the corresponding measurement period in days.
- Calculate and write the Aging Offset.
- Upload the application sketch again.
Service Log
One additional benefit of the service tool is that every interaction is reported in a protocol-friendly format. User inputs, confirmations and results can be copied directly from the Serial Monitor and saved as a detailed service log.
This provides a permanent record of all service operations. Date and time setup and all Aging Offset changes remain fully documented and can be reviewed at any time. This can be particularly useful for documenting maintenance work, comparing repeated calibrations or preparing multiple RTC modules. Instead of being a one-time operation, RTC setup becomes a documented and repeatable maintenance process.
Conclusion
The DS3231 Service Tool is more than a utility sketch. It demonstrates a simple development approach: keep infrequent service tasks separate from the application itself.
Instead of permanently embedding setup, calibration and maintenance code into every project, these operations are performed only when they are actually needed. The application sketch remains smaller, cleaner and focused entirely on its primary function.
At the same time, all service operations can be documented, repeated and verified whenever required. Date and time setup, long-term calibration and Aging Offset adjustment become part of a structured maintenance process rather than permanent application code.
While this article focuses on the DS3231, the same philosophy can be applied to many other peripherals. Whenever a function is needed only occasionally, a dedicated service tool may be a cleaner and more maintainable solution than embedding it permanently into the application.
Complete Sketch
The complete DS3231 Service Tool sketch is provided below and can be used immediately.
Apart from the standard Arduino Wire library, no additional libraries are required. The Serial Monitor menu guides the user through all supported operations.
Menu options shown in square brackets (for example [R]) can be entered in either uppercase or lowercase, followed by Return. At every prompt, the current operation can be cancelled by entering E (or e) followed by Return.
The Aging Offset can be changed again at any time, and date and time can be set whenever required. The clock continues to run as long as the RTC is powered by the Arduino or its backup battery. If both power sources are removed, the RTC returns to its default state and must be configured again before use. The Aging Offset also returns to its default value in this case.
As with any software that modifies hardware settings, use this tool at your own risk.
/*
DS3231 Service Tool
Version 1.0
Purpose
-------
Keep RTC setup and especially calibration code out of the application sketch.
Logging
-------
All user inputs, confirmations and results are reported in a
protocol-friendly format.
The Serial Monitor output can therefore be copied and saved as a
detailed service log.
For a complete service log, enable Serial Monitor timestamps
(Arduino IDE 2.x or later). If the RTC date cannot be used as
a reference, add the current date manually.
Required hardware
-----------------
- Arduino Uno R3 (or compatible)
- USB cable for sketch upload
- 5 V DS3231 RTC module
- 4 jumper wires
Other Arduino boards can also be used, provided that their supply
voltage and I²C logic levels are compatible with the RTC module.
If a 3.3 V-only RTC module is used, power it accordingly and ensure
that the I²C interface is electrically compatible.
Connections
-----------
DS3231 -> Arduino Uno R3
VCC -> 5V
GND -> GND
SDA -> A4
SCL -> A5
Serial Monitor
--------------
Baud rate: 115200
Line ending: Newline, Carriage return, or Both NL & CR
Features
--------
- Display date, time and Aging Offset
- Set date
- Set time
- Calculate Aging Offset from measured drift
- Set Aging Offset directly
User interface
--------------
- Serial Monitor only
- Menu-guided operation
- Input validation
- [E] Escape available at every prompt
Important
---------
A working backup battery is required to keep the RTC running
when the main power supply is removed.
Some DS3231 modules include a charging circuit intended for a
rechargeable backup battery. If your module includes such a circuit,
either use a rechargeable battery or disable the charging circuit
before installing a non-rechargeable battery.
If both main power and backup battery power are removed, the RTC
returns to its default state. This also resets the Aging Offset.
Date input validates numeric ranges only.
Month-specific day counts are intentionally not checked.
Aging Offset
------------
The Aging Offset register provides fine frequency trimming of the DS3231.
One step typically changes the frequency by about 0.1 ppm
(device dependent, see the datasheet).
Positive Aging Offset:
RTC runs slower.
Negative Aging Offset:
RTC runs faster.
Approximate manual calculation:
ppm = (time deviation in seconds) /
(measurement period in days × 86400) × 1000000
Aging Offset change ≈ ppm / 0.1
New Aging Offset =
Current Aging Offset + rounded Aging Offset change
Example:
RTC is 1 second slow after 10 days
ppm = -1 / (10 × 86400) × 1000000
= -1.157 ppm
Aging Offset change ≈ -11.6
≈ -12 steps
With a current Aging Offset of +5:
New Aging Offset = +5 + (-12)
= -7
Note
----
This tool does not update the separate DS3231 day-of-week register.
Applications that use this register should calculate and update it separately.
Use at your own risk.
*/
#include <Wire.h>
constexpr uint8_t DS3231_ADDRESS = 0x68;
constexpr uint8_t TIME_REGISTER = 0x00;
constexpr uint8_t DATE_REGISTER = 0x04;
constexpr uint8_t CONTROL_REGISTER = 0x0E;
constexpr uint8_t STATUS_REGISTER = 0x0F;
constexpr uint8_t AGING_OFFSET_REGISTER = 0x10;
constexpr uint8_t CONTROL_CONV_BIT = 0x20;
constexpr uint8_t STATUS_BSY_BIT = 0x04;
constexpr unsigned long TEMPERATURE_CONVERSION_TIMEOUT_MS = 1000;
constexpr size_t INPUT_BUFFER_SIZE = 32;
char inputBuffer[INPUT_BUFFER_SIZE];
size_t inputLength = 0;
bool discardInputUntilLineEnd = false;
bool ignoreNextLineFeed = false;
int8_t pendingAgingOffset = 0;
float calibrationDeviationSeconds = 0.0;
float calibrationPeriodDays = 0.0;
enum class InputMode
{
MainMenu,
SetDate,
SetTime,
CalibrationIntro,
CalibrationDeviation,
CalibrationDays,
SetOffset,
ConfirmOffset
};
InputMode inputMode = InputMode::MainMenu;
uint8_t bcdToDecimal(uint8_t value)
{
return ((value >> 4) * 10) + (value & 0x0F);
}
uint8_t decimalToBcd(uint8_t value)
{
return ((value / 10) << 4) | (value % 10);
}
bool rtcAvailable()
{
Wire.beginTransmission(DS3231_ADDRESS);
return Wire.endTransmission() == 0;
}
bool readRegisters(uint8_t startRegister,
uint8_t *buffer,
uint8_t length)
{
Wire.beginTransmission(DS3231_ADDRESS);
Wire.write(startRegister);
if (Wire.endTransmission(false) != 0)
{
return false;
}
if (Wire.requestFrom(DS3231_ADDRESS, length) != length)
{
return false;
}
for (uint8_t i = 0; i < length; i++)
{
buffer[i] = Wire.read();
}
return true;
}
bool writeRegisters(uint8_t startRegister,
const uint8_t *buffer,
uint8_t length)
{
Wire.beginTransmission(DS3231_ADDRESS);
Wire.write(startRegister);
for (uint8_t i = 0; i < length; i++)
{
Wire.write(buffer[i]);
}
return Wire.endTransmission() == 0;
}
bool readDateTime(uint16_t &year,
uint8_t &month,
uint8_t &day,
uint8_t &hour,
uint8_t &minute,
uint8_t &second)
{
uint8_t data[7];
if (!readRegisters(TIME_REGISTER, data, sizeof(data)))
{
return false;
}
second = bcdToDecimal(data[0] & 0x7F);
minute = bcdToDecimal(data[1] & 0x7F);
// Support both 12-hour and 24-hour register formats.
if (data[2] & 0x40)
{
const bool pm = data[2] & 0x20;
hour = bcdToDecimal(data[2] & 0x1F);
if (hour == 12)
{
hour = 0;
}
if (pm)
{
hour += 12;
}
}
else
{
hour = bcdToDecimal(data[2] & 0x3F);
}
day = bcdToDecimal(data[4] & 0x3F);
month = bcdToDecimal(data[5] & 0x1F);
year = 2000 + bcdToDecimal(data[6]);
return true;
}
bool readAgingOffset(int8_t &offset)
{
uint8_t value;
if (!readRegisters(AGING_OFFSET_REGISTER, &value, 1))
{
return false;
}
offset = static_cast<int8_t>(value);
return true;
}
bool writeDate(uint16_t year, uint8_t month, uint8_t day)
{
const uint8_t data[3] =
{
decimalToBcd(day),
decimalToBcd(month),
decimalToBcd(year - 2000)
};
return writeRegisters(DATE_REGISTER, data, sizeof(data));
}
bool writeTime(uint8_t hour, uint8_t minute, uint8_t second)
{
const uint8_t data[3] =
{
decimalToBcd(second),
decimalToBcd(minute),
decimalToBcd(hour) // Bit 6 remains 0: 24-hour mode.
};
return writeRegisters(TIME_REGISTER, data, sizeof(data));
}
bool writeAgingOffset(int8_t offset)
{
const uint8_t value = static_cast<uint8_t>(offset);
return writeRegisters(AGING_OFFSET_REGISTER, &value, 1);
}
bool waitUntilTemperatureConversionIdle()
{
const unsigned long startTime = millis();
while (millis() - startTime < TEMPERATURE_CONVERSION_TIMEOUT_MS)
{
uint8_t status;
if (!readRegisters(STATUS_REGISTER, &status, 1))
{
return false;
}
if ((status & STATUS_BSY_BIT) == 0)
{
return true;
}
}
return false;
}
bool forceTemperatureConversion()
{
if (!waitUntilTemperatureConversionIdle())
{
return false;
}
uint8_t control;
if (!readRegisters(CONTROL_REGISTER, &control, 1))
{
return false;
}
control |= CONTROL_CONV_BIT;
if (!writeRegisters(CONTROL_REGISTER, &control, 1))
{
return false;
}
const unsigned long startTime = millis();
while (millis() - startTime < TEMPERATURE_CONVERSION_TIMEOUT_MS)
{
uint8_t currentControl;
uint8_t status;
if (!readRegisters(CONTROL_REGISTER, ¤tControl, 1) ||
!readRegisters(STATUS_REGISTER, &status, 1))
{
return false;
}
if ((currentControl & CONTROL_CONV_BIT) == 0 &&
(status & STATUS_BSY_BIT) == 0)
{
return true;
}
}
return false;
}
void printTwoDigits(uint8_t value)
{
if (value < 10)
{
Serial.print('0');
}
Serial.print(value);
}
void printSignedValue(int8_t value)
{
if (value >= 0)
{
Serial.print('+');
}
Serial.print(value);
}
void printValidInput(const char *input)
{
Serial.print(F("Input \""));
Serial.print(input);
Serial.println(F("\" valid."));
}
void printInvalidInput(const char *input,
const __FlashStringHelper *expected)
{
Serial.print(F("Input \""));
Serial.print(input);
Serial.println(F("\" invalid."));
Serial.print(F("Use "));
Serial.println(expected);
}
bool parseFloatValue(const char *input, float &value)
{
if (input[0] == '\0')
{
return false;
}
char *endPointer;
value = strtod(input, &endPointer);
if (*endPointer != '\0' || !isfinite(value))
{
return false;
}
return true;
}
void printValidEnter()
{
Serial.println(F("Input \"<Enter>\" valid."));
}
void promptForCalibration()
{
Serial.println();
Serial.println(F("Calibration requires:"));
Serial.println(F("- Measured time deviation in seconds (float)"));
Serial.println(F("- Measurement period in days (float)"));
Serial.println();
Serial.println(F("Positive deviation: RTC is fast."));
Serial.println(F("Negative deviation: RTC is slow."));
Serial.println();
Serial.println(F("Press <Enter> to continue."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
inputMode = InputMode::CalibrationIntro;
}
void promptForCalibrationDeviation()
{
Serial.println();
Serial.println(F("Enter measured time deviation in seconds."));
Serial.println(F("Use a positive or negative float value."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
inputMode = InputMode::CalibrationDeviation;
}
void promptForCalibrationDays()
{
Serial.println();
Serial.println(F("Enter measurement period in days."));
Serial.println(F("Use a float value greater than 0."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
inputMode = InputMode::CalibrationDays;
}
void printMenu()
{
Serial.println(F("[R] Refresh data"));
Serial.println(F("[D] set Date"));
Serial.println(F("[T] set Time"));
Serial.println(F("[C] Calculate aging offset (calibration)"));
Serial.println(F("[O] set aging Offset directly"));
}
void refreshData()
{
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
int8_t agingOffset;
if (!readDateTime(year, month, day, hour, minute, second) ||
!readAgingOffset(agingOffset))
{
Serial.println(F("RTC read error."));
return;
}
Serial.println();
Serial.println(F("============================================================"));
Serial.print(year);
Serial.print('-');
printTwoDigits(month);
Serial.print('-');
printTwoDigits(day);
Serial.print(' ');
printTwoDigits(hour);
Serial.print(':');
printTwoDigits(minute);
Serial.print(':');
printTwoDigits(second);
Serial.print(F(" | Aging Offset: "));
printSignedValue(agingOffset);
Serial.println();
Serial.println();
printMenu();
inputMode = InputMode::MainMenu;
}
void promptForDate()
{
Serial.println();
Serial.println(F("Enter date as YYYY-MM-DD."));
Serial.println(F("Year: 2000 to 2099, Month: 1 to 12, Day: 1 to 31."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
inputMode = InputMode::SetDate;
}
void promptForTime()
{
Serial.println();
Serial.println(F("Enter time as HH:MM:SS."));
Serial.println(F("Hour: 0 to 23, Minute: 0 to 59, Second: 0 to 59."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
inputMode = InputMode::SetTime;
}
void promptForOffset()
{
int8_t currentOffset;
Serial.println();
if (!readAgingOffset(currentOffset))
{
Serial.println(F("RTC read error."));
refreshData();
return;
}
Serial.print(F("Current Aging Offset: "));
printSignedValue(currentOffset);
Serial.println();
Serial.println(F("Enter new Aging Offset."));
Serial.println(F("Use -128 to 127 (integer)."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
inputMode = InputMode::SetOffset;
}
bool isEscapeCommand(const char *input)
{
return input[0] != '\0' &&
input[1] == '\0' &&
(input[0] == 'E' || input[0] == 'e');
}
bool parseDate(const char *input,
uint16_t &year,
uint8_t &month,
uint8_t &day)
{
if (strlen(input) != 10 ||
input[4] != '-' ||
input[7] != '-')
{
return false;
}
for (uint8_t i = 0; i < 10; i++)
{
if (i == 4 || i == 7)
{
continue;
}
if (input[i] < '0' || input[i] > '9')
{
return false;
}
}
year =
static_cast<uint16_t>(input[0] - '0') * 1000 +
static_cast<uint16_t>(input[1] - '0') * 100 +
static_cast<uint16_t>(input[2] - '0') * 10 +
static_cast<uint16_t>(input[3] - '0');
month =
static_cast<uint8_t>(input[5] - '0') * 10 +
static_cast<uint8_t>(input[6] - '0');
day =
static_cast<uint8_t>(input[8] - '0') * 10 +
static_cast<uint8_t>(input[9] - '0');
return year >= 2000 && year <= 2099 &&
month >= 1 && month <= 12 &&
day >= 1 && day <= 31;
}
bool parseTime(const char *input,
uint8_t &hour,
uint8_t &minute,
uint8_t &second)
{
if (strlen(input) != 8 ||
input[2] != ':' ||
input[5] != ':')
{
return false;
}
for (uint8_t i = 0; i < 8; i++)
{
if (i == 2 || i == 5)
{
continue;
}
if (input[i] < '0' || input[i] > '9')
{
return false;
}
}
hour =
static_cast<uint8_t>(input[0] - '0') * 10 +
static_cast<uint8_t>(input[1] - '0');
minute =
static_cast<uint8_t>(input[3] - '0') * 10 +
static_cast<uint8_t>(input[4] - '0');
second =
static_cast<uint8_t>(input[6] - '0') * 10 +
static_cast<uint8_t>(input[7] - '0');
return hour <= 23 &&
minute <= 59 &&
second <= 59;
}
bool parseOffset(const char *input, int8_t &offset)
{
if (input[0] == '\0')
{
return false;
}
char *endPointer;
const long value = strtol(input, &endPointer, 10);
if (*endPointer != '\0')
{
return false;
}
if (value < -128 || value > 127)
{
return false;
}
offset = static_cast<int8_t>(value);
return true;
}
void handleMainMenu(const char *input)
{
if (input[0] == '\0' || input[1] != '\0')
{
Serial.println();
printInvalidInput(input, F("R, D, T, C or O."));
refreshData();
return;
}
char command = input[0];
if (command >= 'a' && command <= 'z')
{
command -= 'a' - 'A';
}
switch (command)
{
case 'R':
Serial.println();
printValidInput(input);
refreshData();
break;
case 'D':
Serial.println();
printValidInput(input);
promptForDate();
break;
case 'T':
Serial.println();
printValidInput(input);
promptForTime();
break;
case 'C':
Serial.println();
printValidInput(input);
promptForCalibration();
break;
case 'O':
Serial.println();
printValidInput(input);
promptForOffset();
break;
default:
Serial.println();
printInvalidInput(input, F("R, D, T, C or O."));
refreshData();
break;
}
}
void handleSetDate(const char *input)
{
if (isEscapeCommand(input))
{
Serial.println();
printValidInput(input);
refreshData();
return;
}
uint16_t year;
uint8_t month;
uint8_t day;
if (!parseDate(input, year, month, day))
{
Serial.println();
printInvalidInput(input, F("YYYY-MM-DD."));
Serial.println(F("Year: 2000 to 2099, Month: 1 to 12, Day: 1 to 31."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
return;
}
Serial.println();
printValidInput(input);
if (!writeDate(year, month, day))
{
Serial.println();
Serial.println(F("RTC write error."));
refreshData();
return;
}
Serial.println();
Serial.println(F("Date successfully updated."));
refreshData();
}
void handleSetTime(const char *input)
{
if (isEscapeCommand(input))
{
Serial.println();
printValidInput(input);
refreshData();
return;
}
uint8_t hour;
uint8_t minute;
uint8_t second;
if (!parseTime(input, hour, minute, second))
{
Serial.println();
printInvalidInput(input, F("HH:MM:SS."));
Serial.println(F("Hour: 0 to 23, Minute: 0 to 59, Second: 0 to 59."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
return;
}
Serial.println();
printValidInput(input);
if (!writeTime(hour, minute, second))
{
Serial.println();
Serial.println(F("RTC write error."));
refreshData();
return;
}
Serial.println();
Serial.println(F("Time successfully updated."));
refreshData();
}
void handleCalibrationIntro(const char *input)
{
if (isEscapeCommand(input))
{
Serial.println();
printValidInput(input);
refreshData();
return;
}
if (input[0] != '\0')
{
Serial.println();
printInvalidInput(input, F("<Enter> or E."));
Serial.println(F("Press <Enter> to continue."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
return;
}
Serial.println();
printValidEnter();
promptForCalibrationDeviation();
}
void handleCalibrationDeviation(const char *input)
{
if (isEscapeCommand(input))
{
Serial.println();
printValidInput(input);
refreshData();
return;
}
float value;
if (!parseFloatValue(input, value))
{
Serial.println();
printInvalidInput(input, F("a positive or negative float value."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
return;
}
Serial.println();
printValidInput(input);
calibrationDeviationSeconds = value;
promptForCalibrationDays();
}
void handleCalibrationDays(const char *input)
{
if (isEscapeCommand(input))
{
Serial.println();
printValidInput(input);
refreshData();
return;
}
float value;
if (!parseFloatValue(input, value) || value <= 0.0)
{
Serial.println();
printInvalidInput(input, F("a float value greater than 0."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
return;
}
Serial.println();
printValidInput(input);
calibrationPeriodDays = value;
int8_t currentOffset;
if (!readAgingOffset(currentOffset))
{
Serial.println();
Serial.println(F("RTC read error."));
refreshData();
return;
}
const float ppm =
calibrationDeviationSeconds /
(calibrationPeriodDays * 86400.0) *
1000000.0;
// Typical DS3231 Aging Offset sensitivity: approximately 0.1 ppm per step.
// Positive offset slows a fast RTC; negative offset speeds up a slow RTC.
const float offsetSteps = ppm / 0.1;
const float suggestedChange = roundf(offsetSteps);
const float suggestedOffset =
static_cast<float>(currentOffset) + suggestedChange;
if (!isfinite(ppm) ||
!isfinite(offsetSteps) ||
!isfinite(suggestedChange) ||
!isfinite(suggestedOffset))
{
Serial.println();
Serial.println(F("Calculation result outside supported range."));
Serial.println(F("No update possible."));
refreshData();
return;
}
Serial.println();
Serial.print(F("Measured deviation: "));
if (calibrationDeviationSeconds >= 0.0)
{
Serial.print('+');
}
Serial.print(calibrationDeviationSeconds, 3);
Serial.print(F(" seconds in "));
Serial.print(calibrationPeriodDays, 3);
Serial.println(F(" days"));
Serial.print(F("Calculated deviation: "));
if (ppm >= 0.0)
{
Serial.print('+');
}
Serial.print(ppm, 3);
Serial.println(F(" ppm"));
Serial.print(F("Current Aging Offset: "));
printSignedValue(currentOffset);
Serial.println();
Serial.print(F("Suggested offset change: "));
if (suggestedChange >= 0.0)
{
Serial.print('+');
}
Serial.println(suggestedChange, 0);
if (suggestedChange == 0.0)
{
Serial.println();
Serial.println(F("No Aging Offset update required."));
refreshData();
return;
}
Serial.print(F("Suggested Aging Offset: "));
if (suggestedOffset >= 0.0)
{
Serial.print('+');
}
Serial.println(suggestedOffset, 0);
if (suggestedOffset < -128.0 || suggestedOffset > 127.0)
{
Serial.println();
Serial.println(F("Outside valid range (-128 to +127)."));
Serial.println(F("No update possible."));
refreshData();
return;
}
pendingAgingOffset = static_cast<int8_t>(suggestedOffset);
Serial.println();
Serial.print(F("Set Aging Offset to "));
printSignedValue(pendingAgingOffset);
Serial.println(F("?"));
Serial.println(F("[Y] Yes"));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
inputMode = InputMode::ConfirmOffset;
}
void handleSetOffset(const char *input)
{
if (isEscapeCommand(input))
{
Serial.println();
printValidInput(input);
refreshData();
return;
}
int8_t offset;
if (!parseOffset(input, offset))
{
Serial.println();
printInvalidInput(input, F("-128 to 127 (integer)."));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
return;
}
Serial.println();
printValidInput(input);
pendingAgingOffset = offset;
Serial.println();
Serial.print(F("Set Aging Offset to "));
printSignedValue(pendingAgingOffset);
Serial.println(F("?"));
Serial.println(F("[Y] Yes"));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
inputMode = InputMode::ConfirmOffset;
}
void handleConfirmOffset(const char *input)
{
if (isEscapeCommand(input))
{
Serial.println();
printValidInput(input);
refreshData();
return;
}
if (input[0] == '\0' || input[1] != '\0')
{
Serial.println();
printInvalidInput(input, F("Y or E."));
Serial.println(F("[Y] Yes"));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
return;
}
char command = input[0];
if (command >= 'a' && command <= 'z')
{
command -= 'a' - 'A';
}
if (command != 'Y')
{
Serial.println();
printInvalidInput(input, F("Y or E."));
Serial.println(F("[Y] Yes"));
Serial.println(F("[E] Escape"));
Serial.print(F("> "));
return;
}
Serial.println();
printValidInput(input);
if (!writeAgingOffset(pendingAgingOffset))
{
Serial.println();
Serial.println(F("RTC write error."));
refreshData();
return;
}
if (!forceTemperatureConversion())
{
Serial.println();
Serial.println(F("Aging Offset written, but immediate activation failed."));
refreshData();
return;
}
Serial.println();
Serial.println(F("Aging Offset successfully updated."));
refreshData();
}
void processInput(char *input)
{
char *start = input;
// Remove spaces and tabs at the beginning.
while (*start == ' ' || *start == '\t')
{
start++;
}
// Remove spaces and tabs at the end.
char *end = start + strlen(start);
while (end > start &&
(end[-1] == ' ' || end[-1] == '\t'))
{
end--;
}
*end = '\0';
switch (inputMode)
{
case InputMode::MainMenu:
handleMainMenu(start);
break;
case InputMode::SetDate:
handleSetDate(start);
break;
case InputMode::SetTime:
handleSetTime(start);
break;
case InputMode::CalibrationIntro:
handleCalibrationIntro(start);
break;
case InputMode::CalibrationDeviation:
handleCalibrationDeviation(start);
break;
case InputMode::CalibrationDays:
handleCalibrationDays(start);
break;
case InputMode::SetOffset:
handleSetOffset(start);
break;
case InputMode::ConfirmOffset:
handleConfirmOffset(start);
break;
}
}
void setup()
{
Serial.begin(115200);
Wire.begin();
delay(500);
Serial.println(F("DS3231 Service Tool"));
if (!rtcAvailable())
{
Serial.println();
Serial.println(F("RTC not found."));
while (true)
{
delay(1000);
}
}
refreshData();
}
void loop()
{
while (Serial.available() > 0)
{
const char received = Serial.read();
// Treat CR+LF as one line ending.
if (ignoreNextLineFeed)
{
ignoreNextLineFeed = false;
if (received == '\n')
{
continue;
}
}
if (discardInputUntilLineEnd)
{
if (received == '\r' || received == '\n')
{
discardInputUntilLineEnd = false;
inputLength = 0;
if (received == '\r')
{
ignoreNextLineFeed = true;
}
}
continue;
}
if (received == '\r' || received == '\n')
{
if (inputLength > 0 ||
inputMode == InputMode::CalibrationIntro)
{
inputBuffer[inputLength] = '\0';
processInput(inputBuffer);
inputLength = 0;
}
if (received == '\r')
{
ignoreNextLineFeed = true;
}
continue;
}
if (inputLength < INPUT_BUFFER_SIZE - 1)
{
inputBuffer[inputLength++] = received;
}
else
{
inputBuffer[inputLength] = '\0';
Serial.println();
printInvalidInput(inputBuffer, F("a shorter input."));
refreshData();
inputLength = 0;
discardInputUntilLineEnd = true;
}
}
}
Final Tip
If your project already includes a way to set the RTC date and time (both in hardware and software), it is worth permanently incorporating the Aging Offset determined with this service tool into your application.
This requires only a few additional lines of code:
constexpr int8_t RTC_AGING_OFFSET = -12; // Example value
In setup() after Wire.begin():
Wire.beginTransmission(0x68);
Wire.write(0x10);
Wire.write((uint8_t)RTC_AGING_OFFSET);
Wire.endTransmission();
The cast to uint8_t ensures that the signed 8-bit value is written as the single byte expected by the DS3231 Aging Offset register.
This automatically restores the calibrated Aging Offset every time the application starts. As a result, the calibration is preserved even if the backup battery is replaced or becomes completely discharged. If the RTC has lost power completely, the date and time must still be set again, but the calibrated Aging Offset is restored automatically.
