Interface DS3231 Precision RTC Module with Arduino

We all know that most MCUs we use for our projects are time-agnostic; simply put they are unaware of the time around them. It’s OK for most of our projects but once in a while when you come across an idea where keeping time is a prime concern, DS3231 Precision RTC module is a savior. It’s perfect for projects containing data-logging, clock-building, time stamping, timers and alarms.

DS3231 RTC chip

At the heart of the module is a low-cost, extremely accurate RTC chip from Maxim – DS3231. It manages all timekeeping functions and features a simple two-wire I2C interface which can be easily interfaced with any microcontroller of your choice.

DS3231 RTC Module Chip

The chip maintains seconds, minutes, hours, day, date, month, and year information. The date at the end of the month is automatically adjusted for months with fewer than 31 days, including corrections for leap year (valid up to 2100).

The clock operates in either the 24-hour or 12-hour format with an AM/PM indicator. It also provides two programmable time-of-day alarms.

The other cool feature of this board comes with SQW pin, which outputs a nice square wave at either 1Hz, 4kHz, 8kHz or 32kHz and can be handled programmatically. This can further be used as an interrupt due to alarm condition in many time-based applications.

Temperature Compensated Crystal Oscillator(TCXO)

Most RTC modules come with an external 32kHz crystal for time-keeping. But the problem with these crystals is that external temperature can affect their oscillation frequency. This change in frequency can be negligible but it surely adds up.

To avoid such slight drifts in crystal, DS3231 is driven by a 32kHz temperature compensated crystal oscillator (TCXO). It’s highly immune to the external temperature changes.

TCXO Crystal Oscillator Compensation

TCXO is packaged inside the RTC chip, making the whole package bulky. Right next to the integrated crystal is a temperature sensor.

This sensor compensates the frequency changes by adding or removing clock ticks so that the timekeeping stays on track.

That’s the reason TCXO provides a stable and accurate reference clock, and maintains the RTC to within ±2 minutes per year accuracy.

DS3231 Vs DS1307

The main difference between the DS3231 and DS1370 is the accuracy of time-keeping.

DS1307 comes with an external 32kHz crystal for time-keeping whose oscillation frequency is easily affected by external temperature. This usually results with the clock being off by around five or so minutes per month.

However, the DS3231 is much more accurate, as it comes with an internal Temperature Compensated Crystal Oscillator(TCXO) which isn’t affected by temperature, making it accurate down to a few minutes per year at the most.

DS1307 is still a great value RTC and serves you well, but for projects that require more accurate time-keeping, DS3231 is recommended.

Battery Backup

The DS3231 incorporates a battery input, and maintains accurate timekeeping when main power to the device is interrupted.

The built-in power-sense circuit continuously monitors the status of VCC to detect power failures and automatically switches to the backup supply. So, you need not worry about power outages, your MCU can still keep track of time.

CR2032 Battery Holder on DS3231 RTC Module

The bottom side of the board holds a battery holder for 20mm 3V lithium coincells. Any CR2032 battery can fit well.

Assuming a fully charged CR2032 battery with capacity 220mAh is used and chip consumes its minimum 3µA, the battey can keep the RTC running for a minimum of 8 years without an external 5V power supply.

220mAh/3µA = 73333.34 hours = 3055.56 days = 8.37 years

Onboard 24C32 EEPROM

DS3231 RTC module also comes with a 32 bytes 24C32 EEPROM chip from Atmel having unlimited read-write cycles. It can be used to save settings or really anything.

24C32 EEPROM & I2C Address Selection Jumpers on DS3231 RTC Module

The 24C32 EEPROM uses I2C interface for communication and shares the same I2C bus as DS3231.

The I2C address of the EEPROM can be changed easily with the three A0, A1 and A2 solder jumpers at the back. Each one of these is used to hardcode in the address. If a jumper is shorted with solder, that sets the address.

I2C Address selection jumpers on DS3231 module

As per the 24C32’s datasheet, these 3 bits are placed at the end of the 7-bit I2C address, just before the Read/Write bit.

24C32 EEPROM I2C Address Register

As there are 3 address inputs, which can take 2 states, either HIGH/LOW, we can therefore create 8 (23) different combinations(addresses).

TIP

By default, all the 3 address inputs are pulled HIGH using onboard pullups, giving 24C32 a default I2C address of 1010111Binary or 0x57Hex.

By shorting the solder jumpers, the address inputs are puled LOW. It allows you to set the I2C address according to below table.

24C32 EEPROM I2CAddress Selection Jumpers on DS3231 RTC module

Code for reading/writing onboard 24C32 EEPROM is given at the end of the tutorial.

DS3231 RTC Module Pinout

The DS3231 RTC module has total 6 pins that interface it to the outside world. The connections are as follows:

DS3231 RTC Module Pinout

32K pin outputs the stable(temperature compensated) and accurate reference clock.

SQW pin outputs a nice square wave at either 1Hz, 4kHz, 8kHz or 32kHz and can be handled programmatically. This can further be used as an interrupt due to alarm condition in many time-based applications.

SCL is a serial clock pin for I2C interface.

SDA is a serial data pin for I2C interface.

VCC pin supplies power for the module. It can be anywhere between 3.3V to 5.5V.

GND is a ground pin.

Wiring DS3231 RTC module to Arduino UNO

Let’s hook the RTC up to the Arduino.

Connections are fairly simple. Start by connecting VCC pin to the 5V output on the Arduino and connect GND to ground.

Now we are remaining with the pins that are used for I2C communication. Note that each Arduino Board has different I2C pins which should be connected accordingly. On the Arduino boards with the R3 layout, the SDA (data line) and SCL (clock line) are on the pin headers close to the AREF pin. They are also known as A5 (SCL) and A4 (SDA).

If you have a Mega, the pins are different! You’ll want to use digital 21 (SCL) and 20 (SDA). Refer below table for quick understanding.

SCLSDA
Arduino UnoA5A4
Arduino NanoA5A4
Arduino Mega2120
Leonardo/Micro32

The following diagram shows you how to wire everything.

Wiring DS3231 RTC module with Arduino
Wiring DS3231 RTC module with Arduino

Installing RTClib library

Communicating with a RTC module is a bunch of work. Fortunately, RTClib library was written to hide away all the complexities so that we can issue simple commands to read the RTC data.

To install the library navigate to the Sketch > Include Library > Manage Libraries…Wait for Library Manager to download libraries index and update list of installed libraries.

Arduino Library Installation - Selecting Manage Libraries in Arduino IDE

Filter your search by typing ‘rtclib’. There should be a couple entries. Look for RTClib by Adafruit. Click on that entry, and then select Install.

Installing RTClib Arduino library

Arduino Code – Reading Date & Time

The following sketch will give you complete understanding on how to set/read date & time on DS3231 RTC module and can serve as the basis for more practical experiments and projects.

#include <Wire.h>
#include "RTClib.h"

RTC_DS3231 rtc;

char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

void setup () 
{
  Serial.begin(9600);
  delay(3000); // wait for console opening

  if (! rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while (1);
  }

  if (rtc.lostPower()) {
    Serial.println("RTC lost power, lets set the time!");
	
	// Comment out below lines once you set the date & time.
    // Following line sets the RTC to the date & time this sketch was compiled
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
	
    // Following line sets the RTC with an explicit date & time
    // for example to set January 27 2017 at 12:56 you would call:
    // rtc.adjust(DateTime(2017, 1, 27, 12, 56, 0));
  }
}

void loop () 
{
    DateTime now = rtc.now();
    
    Serial.println("Current Date & Time: ");
    Serial.print(now.year(), DEC);
    Serial.print('/');
    Serial.print(now.month(), DEC);
    Serial.print('/');
    Serial.print(now.day(), DEC);
    Serial.print(" (");
    Serial.print(daysOfTheWeek[now.dayOfTheWeek()]);
    Serial.print(") ");
    Serial.print(now.hour(), DEC);
    Serial.print(':');
    Serial.print(now.minute(), DEC);
    Serial.print(':');
    Serial.print(now.second(), DEC);
    Serial.println();
    
    Serial.println("Unix Time: ");
    Serial.print("elapsed ");
    Serial.print(now.unixtime());
    Serial.print(" seconds/");
    Serial.print(now.unixtime() / 86400L);
    Serial.println(" days since 1/1/1970");
    
    // calculate a date which is 7 days & 30 seconds into the future
    DateTime future (now + TimeSpan(7,0,0,30));
    
    Serial.println("Future Date & Time (Now + 7days & 30s): ");
    Serial.print(future.year(), DEC);
    Serial.print('/');
    Serial.print(future.month(), DEC);
    Serial.print('/');
    Serial.print(future.day(), DEC);
    Serial.print(' ');
    Serial.print(future.hour(), DEC);
    Serial.print(':');
    Serial.print(future.minute(), DEC);
    Serial.print(':');
    Serial.print(future.second(), DEC);
    Serial.println();
    
    Serial.println();
    delay(1000);
}

Here’s how the output looks like in the serial monitor.

DS3231 Output On Serial Monitor

Code Explanation:

The sketch starts with including wire.h & RTClib.h libraries for communicating with the module. We then create an object of RTClib library and define daysOfTheWeek 2D character array to store days information.

In setup and loop sections of the code, we use following functions to interact with the RTC module.

begin() function ensures that the RTC module is connected.

lostPower() function reads the DS3231’s internal I2C registers to check if the chip has lost track of time. If the function returns true, we can then set the date & time.

adjust() function sets the date & time. This is an overloaded function.

  • One overloaded method DateTime(F(__DATE__), F(__TIME__)) sets the date & time at which the sketch was compiled.
  • Second overloaded method DateTime(YYYY, M, D, H, M, s) sets the RTC with an explicit date & time. For example to set January 27 2017 at 12:56 you would call: rtc.adjust(DateTime(2017, 1, 27, 12, 56, 0));

now() function returns current date & time. Its return value is usually stored in the variable of datatype DateTime.

year() function returns current year.

month() function returns current month.

day() function returns current day.

dayOfTheWeek() function returns current day of the week. This function is usually used as an index of a 2D character array that stores days information like the one defined in above program daysOfTheWeek

hour() function returns current hour.

minute() function returns current minute.

second() function returns current seconds.

unixtime() function returns unix time in seconds. Unix time is a system for describing a point in time. It is the number of seconds that have elapsed since 00:00:00(known as Coordinated Universal Time – Thursday, 1 January 1970).

TimeSpan() function is used to add/subtract time to/from current time. You can add/subtract days, hours, minutes & seconds. It’s also an overloaded function.

  • now() + TimeSpan(seconds) returns the future time with seconds added into current time.
  • now() - TimeSpan(days,hours, minutes, seconds) returns the past time.

Arduino Code – Reading/Writing in 24C32 EEPROM

With DS3231 RTC module, as a bonus, you get 32 bytes of Electrically Erasable ROM. Its contents will not be erased even if main power to the device is interrupted.

The following program writes and then reads a message from the 24C32 EEPROM. You can use this program to save settings or passwords or really anything.

#include <Wire.h>

void setup()
{
    char somedata[] = "lastminuteengineers.com"; // data to write
    Wire.begin(); // initialise the connection
    Serial.begin(9600);
    Serial.println("Writing into memory...");
	
	// write to EEPROM
    i2c_eeprom_write_page(0x57, 0, (byte *)somedata, sizeof(somedata));

    delay(100); //add a small delay
    Serial.println("Memory written");
}

void loop()
{
    Serial.print("Reading memory: ");
    int addr=0; //first address
	
	// access the first address from the memory
    byte b = i2c_eeprom_read_byte(0x57, 0);

    while (b!=0)
    {
        Serial.print((char)b); //print content to serial port
        addr++; //increase address
        b = i2c_eeprom_read_byte(0x57, addr); //access an address from the memory
    }
    Serial.println(" ");
    delay(2000);
}

void i2c_eeprom_write_byte( int deviceaddress, unsigned int eeaddress, byte data ) {
    int rdata = data;
    Wire.beginTransmission(deviceaddress);
    Wire.write((int)(eeaddress >> 8)); // MSB
    Wire.write((int)(eeaddress & 0xFF)); // LSB
    Wire.write(rdata);
    Wire.endTransmission();
}

// WARNING: address is a page address, 6-bit end will wrap around
// also, data can be maximum of about 30 bytes, because the Wire library has a buffer of 32 bytes
void i2c_eeprom_write_page( int deviceaddress, unsigned int eeaddresspage, byte* data, byte length ) {
    Wire.beginTransmission(deviceaddress);
    Wire.write((int)(eeaddresspage >> 8)); // MSB
    Wire.write((int)(eeaddresspage & 0xFF)); // LSB
    byte c;
    for ( c = 0; c < length; c++)
        Wire.write(data[c]);
    Wire.endTransmission();
}

byte i2c_eeprom_read_byte( int deviceaddress, unsigned int eeaddress ) {
    byte rdata = 0xFF;
    Wire.beginTransmission(deviceaddress);
    Wire.write((int)(eeaddress >> 8)); // MSB
    Wire.write((int)(eeaddress & 0xFF)); // LSB
    Wire.endTransmission();
    Wire.requestFrom(deviceaddress,1);
    if (Wire.available()) rdata = Wire.read();
    return rdata;
}

// maybe let's not read more than 30 or 32 bytes at a time!
void i2c_eeprom_read_buffer( int deviceaddress, unsigned int eeaddress, byte *buffer, int length ) {
    Wire.beginTransmission(deviceaddress);
    Wire.write((int)(eeaddress >> 8)); // MSB
    Wire.write((int)(eeaddress & 0xFF)); // LSB
    Wire.endTransmission();
    Wire.requestFrom(deviceaddress,length);
    int c = 0;
    for ( c = 0; c < length; c++ )
        if (Wire.available()) buffer[c] = Wire.read();
}

Here’s how the output looks like in the serial monitor.

24C32 EEPROM Output On Serial Monitor