Getting to grips with a Real Time Clock

Introduction

Eventually you may need to keep track of time in your projects; once you can do this reliably it opens plenty of possibilities for better, smarter and more functional creations.  Keeping track of time accurately is no trivial task but thankfully there’s no need for us to reinvent the wheel as it were.  There are plenty of manufacturers that provide pretty damn accurate Real Time Clock functionality all wrapped up in a an integrated circuit, most of which require very little additional circuitry to get going.  Normally a couple of resistors and / or capacitors, and of course an oscillator to give it a means of timing.  Maxim provide a range of RTC IC’s, many of which go one step further  in helping us out by providing its own inbuilt oscillator, that just makes good sense to me!  Their range provides IC’s with various standards and differing communication systems.  For this article I’ll be referring to their DS3231 IC.  Here’s what they have to say about it…

ds3231sn#The DS3231 is a low-cost, extremely accurate I²C real-time clock (RTC) with an integrated temperature-compensated crystal oscillator (TCXO) and crystal. The device incorporates a battery input, and maintains accurate timekeeping when main power to the device is interrupted. The integration of the crystal resonator enhances the long-term accuracy of the device as well as reduces the piece-part count in a manufacturing line. The DS3231 is available in commercial and industrial temperature ranges, and is offered in a 16-pin, 300-mil SO package.”

The RTC 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. The clock operates in either the 24-hour or 12-hour format with an active-low AM/PM indicator. Two programmable time-of-day alarms and a programmable square-wave output are provided. Address and data are transferred serially through an I²C bidirectional bus.

A precision temperature-compensated voltage reference and comparator circuit monitors the status of VCC to detect power failures, to provide a reset output, and to automatically switch to the backup supply when necessary. Additionally, the active-low RST pin is monitored as a pushbutton input for generating a µP reset.”

The DS series is actually quite popular, its accuracy and functionality are perfect for most people’s needs.  Unfortunately like any IC, its nuances can be confusing especially if you aren’t experienced with dealing with registers and working with binary coded decimal.   Hopefully this text will help you out.  By the time we’re done here, you’ll be able to write your own firmware for one.   Please be aware that the code examples I use here were adapted as part of an on-going project of mine, so please do feel free to experiment and roll your own more specialised library for yourself.  Ok, enough talk, let’s get into this.

The first thing to realise is that the DS3231 has a set of 19, 8 bit registers that hold its data.

Address Bit 7 (MSB) Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
(LSB)
Function
0x00 Seconds
0x01 Minutes
0x02 12/24 AM/PM Hours
0x03 Day
0x04 Date
0x05 Month
0x06 Year
0x07 Alarm 1 Seconds
0x08 Alarm 1 Minutes
0x09 12/24 AM/PM Alarm 1 Hours
0x0A DY/DT Alarm 1 Date / Day
0x0B Alarm 2 Minutes
0x0C 12/24 AM/PM Alarm 2 Hours
0x0D DY/DT Alarm 2 Date / Day
0x0E Control
0x0F Status
0x10 Aging Offset
0x11 Temperature (MSB)
0x12 Temperature (LSB)

Things to note about this memory map.

  • The data in the first 14 registers (0x00 to 0x0D) is stored in Binary coded decimal format, whereas the data if the last 5 registers (0x0E to 0x12) is not!
  • Alarm 2 differs from Alarm 1 in that it does not store any value for seconds.
  • “Day” refers to Day of the week, i.e.  1-7, whereas Date refers to Date of the month, i.e 1-31.  The same register is used to store both values; we tell the RTC which we mean by setting one of the other bits of that register DY/DT (bit 6).
  • 12/24 and the AM/PM bits are used just like the Day/Date fields, but indicate whither the number stored in this register is in 12 or 24hour format, and is AM or PM.

It may take you a while to understand these nuances but all that is necessary for now is to be aware of them.  The next two registers are much simpler to deal with since they are not binary encoded values, just individual bits that are set on or off.  Lastly, the next two registers just hold the most significant and least significant parts of the IC’s internal temperature sensor.  So for example, if the current temperature of the IC is 23.75 degrees, the 23 is stored in the MSB register and the .75 is stored in the LSB register.  We just need to pull them apart or put them together when reading or writing to these registers.  Don’t be put off, with a couple of additional functions dealing with this is a piece of cake! 😉

Communicating with the RTC

I don’t intend to cover the I2C protocol itself, please look elsewhere for that kind of detail.  My intention here is to try and get you to understand how the DS3231 works and is used.  I’m leaving the underlying protocol details to the Wire.h Arduino library.  If you’re not using an Arduino based microcontroller you’ll have to use an alternative to this.  Most vendors provide an equivalent since I2C is so common.  However, you could roll your own simply from the information provided in the datasheet from the DS3231.  Let’s get back to it…  The DS series IC’s that use the I2C protocol, basically work in two modes, receiving data or sending data.

Write mode (receiving mode)

  1. The master transmits the address of the RTC.
    This wakes the correct device on the I2C bus.  When the master sends out the address, the last bit indicates whether the master is sending or requesting data.  This is how the slave knows what to expect next.  This part is handled for us by the wire library so you don’t need to do anything, but it’s good to understand that is what is happening in the background.
  2. The master sends the address of the register of interest.
    The slave knows this is a write request, but doesn’t know which register we want. So the next byte of data sent is the register address.  If for example we were interested in the seconds, we would send the address 0x00.  The IC, then moves its register pointer to that address.
  3. The Master sends some data.
    With the IC’s register pointer now set at the correct position, the master starts to send data.  Be aware that every time the register receives a full byte of data, it will automatically increment its pointer to the next register.  So, to write the complete time, we just keep send bytes for seconds, then minutes, then hours etc. The register address just keeps auto incrementing.  If it reaches the end of the registers, it wraps around and points at 0x00 again.  So when reading or writing more than one register at a time, we must ensure that we send or request only the correct number of bytes.
  4. The master ends the transmission.
    The master completes the conversation by sending a stop signal.

Read mode (sending mode)

When reading data from the IC, the procedure is almost the same as writing, except once we’ve set the IC’s register pointer we send a stop signal, then begin reading the data.  The communication ends when the IC has sent the number of bytes requested.

Let’s look at a couple of example snippets that show how easy this is when using the wire library.  Here is an example of how to read a register…

uint8_t RTC_DS3231::GetRegister(uint8_t reg)
Wire.beginTransmission(DS3231_ADDRESS); // Wake the I2C device
Wire.SEND(reg);                         // Advance its internal register
Wire.endTransmission();                 // End the transmission
Wire.requestFrom(DS3231_ADDRESS, 1);    // Request 1 byte from device
return Wire.RECEIVE();

And here’s how we’d write to a register.

void RTC_DS3231::SetRegister(uint8_t reg, uint8_t byte)
{
    Wire.beginTransmission(DS3231_ADDRESS);// Wake the I2C device
    Wire.SEND(reg);                        // Advance its internal register
    Wire.SEND(byte);                       // Send data bytes...
    Wire.endTransmission();                // End the transmission
}

Note that the only real difference between these two snippets is the way we end the transmission, before reading the data.  Ok, so now we know how to write a full byte to any of the registers that we want, just remember that some of the registers store binary coded values (BCD) rather than normal integer values.  Simply use a function to convert to and from as necessary; (bin2bcd() and bcd2bin() are provided).  Just ensure that you encode the value itself first, and then turn on any indicator bits that you may require.  For example, if you are storing the hour value as 7 pm, you would encode the value ‘7’ then after that, turn on the 12/24 hr (bit 6) and the AM/PM (bit 5).  Confused?  Don’t be, instead take a look at this snippet.

// Convert the required time/date values to BCD values
uint8_t hours   = bin2bcd( dt.hour() );
// Now turn on / off any required bits for that register.
hours |= (1 << 6)                        // Turn on the 6th bit
// transmit to DS3231
Wire.beginTransmission( DS3231_ADDRESS );// Wake the correct I2C device.
Wire.SEND( REGISTER_ADDRESS );           // Advance its internal register
Wire.SEND( hours );                      // Send the (bcd) data.
Wire.endTransmission();                  // End the transmission

So, at this stage you have a good understanding of how the DS3231 stores its data and how to get at each register to read or write a byte at a time from it.  Obviously there may be a need to alter a single bit of a register rather than a whole byte.  To do that, simply read the whole byte, then use bitwise operations to alter the required bits (just like we did in the example above), then write the whole byte back to the register.

Believe it or not, you now know just enough to be able to initialise the RTC chip with the correct time, set either of the two alarms or read and write to any of the available registers.  It’s probably time to put this all this together into something usable.  I’ve knocked together a library with code commented so that if you’ve read this text, you’ll understand what’s going on and more importantly how to alter it if necessary.  There are however just a couple of things I should mention that I haven’t covered so far.

When dealing with the RTC, it’s very convenient to have some kind of time object that you can use to simplify passing date and time values around.  My example library uses a DateTime class to do this.  The code for this class is included.  It was adapted from other open sources and is used only as a convenience.   Here are a couple of snippets that show its usage.

Firstly, an easy way to initialise the RTC to the date the code was compiled

// Sets the RTC to the date & time this sketch was
// compiled if its out of date
DateTime now(2013,12,24,1);
DateTime compiled = DateTime(__DATE__, __TIME__);
if (now.unixtime() & compiled.unixtime())
{
    Serial.println("RTC is older than compile time! Updating");
    RTC.SetDateTime(DateTime(__DATE__, __TIME__));
}

The now() method provides a convenient way to get the current time as a DateTime object.

/* Returns the current time as a DateTime object */
DateTime RTC_DS3231::now()
{
    Wire.beginTransmission(DS3231_ADDRESS);     // Wake the I2C device
    Wire.SEND(0);                               // Advance internal register
    Wire.endTransmission();                     // End the transmission
    Wire.requestFrom(DS3231_ADDRESS, 7);        // Request 7 bytes from device
    uint8_t ss = bcd2bin(Wire.RECEIVE() & 0x7F);// Read the data...
    uint8_t mm = bcd2bin(Wire.RECEIVE());
    uint8_t hh = bcd2bin(Wire.RECEIVE());
    Wire.RECEIVE();                             // Skip the day of week.
    uint8_t d = bcd2bin(Wire.RECEIVE());
    uint8_t m = bcd2bin(Wire.RECEIVE());
    uint16_t y = bcd2bin(Wire.RECEIVE()) + 2000;
    return DateTime (y, m, d, hh, mm, ss);
}

As stated right at the start, there are other reasons for me using this DateTime class that are particular to another project,  feel free to roll your own if required, its only there for convenience and is not needed for communication with the DS3231.   Speaking of convenience, you’ll notice that in the header file provided there are many definitions of registers and bits.  Use these when dealing with registers and their individual bits; it makes your code much cleaner.  For example, instead of this (as we did earlier)

hours |= (1 << 6)       // Turn on the 6th bit

I would use this

hours |= DS3231_BIT_12_24 // Turn on the 6th bit


It’s really easy to confuse the hell out of yourself if you don’t! I think I’ve defined all the bits / registers you’ll need, remember to take a peek at the header file.

Gotchas to look out for!

RTFM (or rather the datasheet) that will answer all your questions, but here are a couple of things that may trip you up simply because you haven’t noticed them.

  • How do I actually check if an alarm has triggered?
    The first two bits (LSB) of the status register indicate if an alarm is triggered, so simply begin a transmission, send the address of the status register (0x0F), read the byte then check the last two bits using a bitwise comparison. Alarm 1 is bit 0 and alarm 2 is bit 1.

    Alternatively set the interrupt flag to ensure that an interrupt is fired. Wire the interrupt output from the RTC to your microcontrollers interrupt pin.  Then handle the interrupt like any other external interrupt. Refer to the datasheet for more details on the interrupt flag bit of the control register.
  • Why didn’t my alarm trigger?
    Not only do you have to set the time / date of the alarm, but you also have to actually switch it on or off.  You do this by setting the last two bits of the control register (0x0E).  If you use the SetAlarm1() or SetAlarm2() methods in my library this is handled for you by the following code:
// Now the time of the alarm is set, turn it on (or off!)
SwitchRegisterBit(DS3231_SPECIAL_CONTROL,DS3231_BIT_A1IE, on );
  • Why does my alarm keep triggering?
    Please take a look at the datasheet for the DS3231 and note that bit 7 of both alarm registers is labelled as A1M1 to A2M2.  These bits set the alarm rate.  The datasheet explains the effect of setting these bits but to surmise, they affect how often an alarm will trigger.  E.g. Every second, every minute or only when the date, hour and minutes match etc.  Take a look at the datasheet for the details.  Once again, if you use the methods in my library this can be set (using the AlarmTriggerType enumeration provided in the header file) or the method defaults to set the bits to match every time the current time matches the Hours, minutes and seconds of Alarm 1; and every time the Hours and minutes for alarm 2.Remember you can always turn each alarm on or off without affecting its time, by setting the correct bit of the control register.  E.g to switch alarm 1 on and alarm 2 off you could use the following code.
SwitchRegisterBit(DS3231_SPECIAL_CONTROL,DS3231_BIT_A1IE, true );
SwitchRegisterBit(DS3231_SPECIAL_CONTROL,DS3231_BIT_A2IE, false );

I think that just about covers it.  If you need a very good, reliable and accurate RTC I’ve had some premade breakout boards made up that use the DS3231SN# RoHS compliant RTC IC from Maxim.  You can use my library available from Github or one of the many other compatible libraries.   Remember this RTC breakout will work with any microcontroller that is I2C capable and needs a 2.2v – 5v supply.  The hard work has been done for you, all you need to do is plug it in and put a battery in it if you want it to retain its time when power is removed.

There are many RTC available and as is often the case they are built to be as cheap as possible.  Mine uses a fantastic temperature controlled one for greater operational accuracy with a built in oscillator.  A battery connector that’s not as cheap as most are and a decent female header so it can be more easily used in a development scenario without soldering & re-soldering.

Have fun!

HC

DS3231 Datasheet

Library files

Advertisements

2 thoughts on “Getting to grips with a Real Time Clock

  1. Thanks for explaining all this so clearly. I had been struggling to find a way to reset just the seconds to zero without altering the other values. None of the other libraries or articles on the DS3231 explained the selection and sequencing of registers.
    For what it’s worth, I was looking for a way to correct the drift on the RTC by using a midnight time signal by wireless from an MSF clock – this was the last link in the chain.
    Thanks again

    • “I was looking for a way to correct the drift on the RTC by using a midnight time signal by wireless from an MSF clock”

      Hmm, that’s interesting. Wish you every success! 😀

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s