Post

ENG | Arduino, DS3231 Real-Time Clock

Comprehensive guide on using the DS3231M real-time clock module with Arduino. Learn to read and set the current time, wake up Arduino from sleep mode using RTC interrupts, and integrate precise timekeeping in your projects. Includes well-explained code examples and troubleshooting tips.

Intro

Data logging is a critical process in many fields, from environmental monitoring to scientific research and industrial applications. At the core of any data logging system is the ability to accurately record data points with precise timestamps. This is where the real-time clock (RTC) module comes into play, ensuring reliable timekeeping even when the device is powered off or reset.

In the world of Arduino projects, the DS3231 module stands out as a popular and versatile solution for incorporating real-time clock functionality. This comprehensive tutorial aims to guide you through the process of integrating the DS3231 into your Arduino-based data logger, unlocking its full potential for accurate timekeeping and efficient power management.

By following this tutorial, you’ll learn how to read the current time from the DS3231, set the time using a Python utility, and leverage the module’s interrupt capabilities to wake up the Arduino from sleep mode. These features not only enable precise time tracking but also contribute to power efficiency, a crucial aspect of long-term data logging applications.

Whether you’re a hobbyist, a researcher, or an embedded systems developer, mastering the DS3231 module will prove invaluable in building robust and efficient data logging systems. So, let’s dive in and explore the world of the DS3231, one step at a time.

ZS-042 RTC Module Warning

Added 2024-03-25

I searched for info about ZS-042 RTC module, because some sites state it needs LIR-2023 battery instead CR-2023 which is obvious choice and that it’s dangerous (🔥💥🧨❗) to use CR-2023 due to charging circuit. The article One Transistor -=- Battery charging circuit of DS3231 module or Arduino Forum -=- ZS-042 DS3231 RTC module suggest that charging circuit is poorly designed and it’s relatively safe to use it at 3.3V regardless of battery (and it won’t be charged), but it’s not friendly to LIR-2023 battery when it’s powered by voltage above 4.7V anyways (because battery’s charging voltage must not exceeded 4.2V) and it’s recommended to disable charging circuit.

ZS-042 module is described later, but I have feeling that this warnings should be on top.

Reading real time clock

Reading real time clock is easy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/// \file   rtc3231_01_read_time
/// \brief  Read time from RTC3231
/// \author Pavel Perina
/// \date   May 2016
///
/// Output (example):
///
/// RTC Reader startup
/// ==================
/// 2000-01-03T22:36:52Z
/// 2000-01-03T22:36:53Z
/// 2000-01-03T22:36:54Z
///
/// Changelog:
/// 2016-05 Initial version
/// 2024-03 Modified to use class

#include <Wire.h>


class Rtc3231
{
public:
  /// \brief  Get datetime of DS3231 RTC, assumes UTC (same date format as GPX file)
  /// \return Pointer to static buffer with time
  static char* dateTimeStr();
private:
  static constexpr int8_t i2cAddr = 0x68;
};


char* Rtc3231::dateTimeStr()
{
  //                        0123456789012345678
  static char datetime[] = "20YY-MM-DDThh:mm:ssZ\0";
  byte bcd;
  Wire.beginTransmission(i2cAddr);
  Wire.write(0);                // set DS3231 register pointer to 00h
  Wire.endTransmission();
  Wire.requestFrom(i2cAddr, 7); // request seven bytes of data from DS3231 starting from register 00h
  // second, minute, hour
  bcd = Wire.read() & 0x7f;     // 00 seconds
  datetime[17] = '0' + (bcd>>4);
  datetime[18] = '0' + (bcd&0x0f);
  bcd = Wire.read();            // 01 minutes
  datetime[14] = '0' + (bcd>>4);
  datetime[15] = '0' + (bcd&0x0f);
  bcd = Wire.read() & 0x3f;     // 02 hours (bit 7 is 12/24, bit 6 is AM/PM
  datetime[11] = '0' + (bcd>>4);
  datetime[12] = '0' + (bcd&0x0f);
  bcd = Wire.read();            // 03 day of week (skip)
  bcd = Wire.read();            // 04 date
  datetime[ 8] = '0' + (bcd>>4);
  datetime[ 9] = '0' + (bcd&0x0f);
  bcd = Wire.read() & 0x7f;     // 05 month
  datetime[ 5] = '0' + (bcd>>4);
  datetime[ 6] = '0' + (bcd&0x0f);
  bcd = Wire.read();            // 06 year
  datetime[ 2] = '0' + (bcd>>4);
  datetime[ 3] = '0' + (bcd&0x0f);
  return datetime;
}


void setup() 
{
  // put your setup code here, to run once:
  Serial.begin(9600);
  while (!Serial)
    ;
  Serial.print("RTC Reader startup\n");
  Serial.print("==================\n");
  Wire.begin();
}


void loop() 
{
  // put your main code here, to run repeatedly:
  Serial.println(Rtc3231::dateTimeStr());  
  delay(1000);
}

Setting real time clock

To set real time clock I made this utility long time ago: rtc_set

It consists of two parts:

  • Arduino program that awaits time in a certain format
  • Python script that connects to Arduino (port has to be modified)

Let’s use it (the following instructions are for Linux, but on Windows it’s similar)

  • First get it
    1
    2
    3
    4
    5
    6
    7
    8
    
    $ cd ~/Arduino
    
    $ git clone [email protected]:pavel-perina/rtc_set.git
    Cloning into 'rtc_set'...
    remote: Enumerating objects: 23, done.
    remote: Total 23 (delta 0), reused 0 (delta 0), pack-reused 23
    Receiving objects: 100% (23/23), 13.14 KiB | 6.57 MiB/s, done.
    Resolving deltas: 100% (7/7), done.
    
  • Second open Arduino IDE, compile and upload it.
  • Then close serial monitor.
  • Modify Python file according to your setup e.g COM6, /dev/ttyUSB0
  • Execute Python file, install pyserial module if needed
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    $ python3 rtc_set.py 
    Traceback (most recent call last):
      File "/home/pavel/Arduino/rtc_set/rtc_set.py", line 1, in <module>
        import  serial
    ModuleNotFoundError: No module named 'serial'
    
    $ pip3 install pyserial
    
    Defaulting to user installation because normal site-packages is not writeable
    Collecting pyserial
      Obtaining dependency information for pyserial from https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl.metadata
      Downloading pyserial-3.5-py2.py3-none-any.whl.metadata (1.6 kB)
    Downloading pyserial-3.5-py2.py3-none-any.whl (90 kB)
      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 90.6/90.6 kB 1.9 MB/s eta 0:00:00
    Installing collected packages: pyserial
    Successfully installed pyserial-3.5
    
    $ ./rtc_set.py 
    Sleeping for five seconds ...
    THU 2024-03-21 19:17:07
    
  • Now you can verify that time was set in Arduino IDE or by connecting to serial and restting arduino
    1
    2
    3
    4
    5
    
    Clearing interrupt and disabling alarm ...
    Current time is : 2024-03-21T19:23:24Z
    Enter date and time in DOW-YYYY-MM-DD-hh-mm-ss format.
    DOW is SUN, MON, TUE, WED, THU, FRI, SAT
    Hyphens are ignored, by the way ...
    

Using DS3231 to wake up Arduino (2016-05-31)

Note that this is more complicated than using internal watchdog timer.

Also note that this is proof of concept and Arduino Nano is just as power hungry as Arduino Uno, it just comes in more practical package (it’s convenient to use with breadboard and it can be soldered into a final product … eh, a gadget).

I have bought two DS3231 boards from Keyestudio, they are perfectly ok, chip has interupt unlike popular DS1307, but … it does not have pin to comfortably use INT/SQW. Here comes soldering with Kapton masking tape.

First goal was to program RTC to trigger interrupt in next minute, to cancel it somehow and repeat with writing pin state. Then to extend it by sleep mode. First test program was lost. https://www.analog.com/media/en/technical-documentation/data-sheets/DS3231M.pdf Pin configuration Pin configuration of DS3231 (source: datasheet)

Pin configuration< Wire from UTP cable soldered to pin 3 on DS3231.

Wire from INT pin should be connected to INT0/INT1 pins on Arduino - these are pins D2,D3. I decided for D3, but there’s no reason. This wire should be connected to VCC via pull-up resistor, or just call pinMode(RTC_INT_PIN, INPUT_PULLUP); to use internal pull-up. Also note that when interrupt is triggered, this input goes from HIGH to LOW. That’s why I call attachInterrupt (RTC_INT_PIN - 2, rtcIntHandler, FALLING); in the code. There’s one funny thing. According to ATmega datasheet only LOW state can be used for interrupt in sleep mode. But it obviously works and using LOW is perhaps little bit complicated, cause we must somehow disable interrupt processing, otherwise Arduino will hang processing interrupt forever. According to http://www.gammon.com.au/interrupts the datasheet is wrong and FALLING can be used. I just tried it and it worked 😀.

Pin configuration Test circuit v2 with Arduino Pro Mini. It start’s to look messy. Note that pins A4, A5, (A6, A7) do not fit into breadboard grid 🫣

Registers are same for many DS#### RTC chips, there are some libraries ready to use, but I decided to look into source code from DeadBugPrototypes DataLogger Shield and into datasheet and written it myself. It’s not that hard and it’s not necessary to cut&paste few lines of code.

RTC registers DS3231X registers, source: datasheet

Code for RTC is quite easy, entering sleep mode is easy. Only harder part was to make interrupt working, cause serial communication is running in background. It won’t print anything, if program freezes and it can’t be used from interrupt handler.

Things learned:

  • Next time I will solder female plug to pins A4,A5 of Arduino Pro Mini in order to use same cables or possibly solder wires directly to board. (Changed opinion: best is to bend them to side in order to keep profile low on protoboard)
  • Interrupts can be triggered by falling edge despite what’s stated in documentation.
  • Chip is actually not completely ok, accuracy is bullshit. One second a day is not 2ppm, not even 5ppm. What to expect from module that costs 1USD on ebay whereas chip costs 4USD if you order 1000 of them. Chinese copy? Taken from pile that did not passed quality control?
  • Keyestudio DS3231 module
    • Module used above is garbage - battery can’t be replaced and INT/SQW pin is not on pin header
    • Never put glue on something that is not tested. When I soldered wire to another chip, I somehow desoldered and lifted pin 2 (vcc), so connecting it to arduino caused it to crash on any I2C communication attempt.
  • ZS-042 DS3231 module
    • 🔥 Forum mentions cases of inflated or exploding battery
    • It needs minor modifications such as desoldering diode used to charge battery with charging circuit being designed for roughly 4V which is never the case and desoldering LED’s resistor for lower power consumption - desoldering LED is impossible, resistor goes away easy.
    • Path between pull up resistor and INT/SQW should be physically interrupted when power consumption is critical and RTC has VCC shut down to run from it’s battery (saves approx 90uA)
    • Small 4kB/32kbit EEPROM can be handy.

Test code

NOTE: intTriggered is not used, remained from interrupt on LOW

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//////////////////////////////////////////////////////////////////
/// \brief  Arduino wake up by interrupt from DS3231 (or similar)
/// \author Pavel Perina
/// \date   May 2016

#include <avr/sleep.h>
#include <Wire.h>

/////////////////////////////////////////////////////////////////
// Utility functions

static uint8_t bcd2bin (uint8_t val) 
{ 
  return val - 6 * (val >> 4); 
}


static uint8_t bin2bcd (uint8_t val)
{ 
  return val + 6 * (val / 10); 
}

///////////////////////////////////////////////////////////////////////
// RTC CODE for DS323X chips
// Note: #include <Wire.h> before this

#define DS3231_I2C_ADDRESS 0x68

/// \brief  Get datetime of DS3231 RTC, assumes UTC (same date format as GPX file)
/// \return Pointer to static buffer with Time
char * rtcDateTimeStr()
{
  //                        0123456789012345678
  static char datetime[] = "20YY-MM-DDThh:mm:ssZ\0";
  byte bcd;
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write(0);                              // set DS3231 register pointer to 00h
  Wire.endTransmission();
  Wire.requestFrom(DS3231_I2C_ADDRESS, 7);    // request seven bytes of data from DS3231 starting from register 00h
  // second, minute, hour
  bcd = Wire.read() & 0x7f; // 00 seconds
  datetime[17] = '0' + (bcd>>4);
  datetime[18] = '0' + (bcd&0x0f);
  bcd = Wire.read();        // 01 minutes
  datetime[14] = '0' + (bcd>>4);
  datetime[15] = '0' + (bcd&0x0f);
  bcd = Wire.read() & 0x3f; // 02 hours (bit 7 is 12/24, bit 6 is AM/PM
  datetime[11] = '0' + (bcd>>4);
  datetime[12] = '0' + (bcd&0x0f);
  bcd = Wire.read();        // 03 day of week
  bcd = Wire.read();        // 04 date
  datetime[ 8] = '0' + (bcd>>4);
  datetime[ 9] = '0' + (bcd&0x0f);
  bcd = Wire.read() & 0x7f; // 05 month
  datetime[ 5] = '0' + (bcd>>4);
  datetime[ 6] = '0' + (bcd&0x0f);
  bcd = Wire.read();        // 06 year
  datetime[ 2] = '0' + (bcd>>4);
  datetime[ 3] = '0' + (bcd&0x0f);
  return datetime;
}

/// \brief  Set DS32XX alarm in X minutes from now
void rtcAlarmInMinutes(uint8_t inMinutes)
{
  Wire.begin(DS3231_I2C_ADDRESS);
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write((byte) 1);   // set address to 01 (minutes)
  Wire.endTransmission();

  Wire.requestFrom(DS3231_I2C_ADDRESS, 1);
  uint8_t mins = bcd2bin(Wire.read());  // 01 minutes
  mins += inMinutes;
  if (mins > 59) 
    mins -= 60; 
  
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  // A1M4=1 A1M3=1 A1M2=0 A1M2=0 -> alarm when minutes and seconds match
  Wire.write((byte) 7);
  Wire.write(0b00000000);     // 07 A1M1, seconds
  Wire.write(bin2bcd(mins));  // 08 A1M2, minutes
  Wire.write(0b10000000);     // 09 A1M3, hours
  Wire.write(0b10000000);     // 0A A1M4, day
  Wire.write(0b00000000);     // 0B A2M2, minutes
  Wire.write(0b00000000);     // 0C A2M3, hours
  Wire.write(0b00000000);     // 0D A2M4, day
  Wire.write(0b00000101);     // 0E INTCN-int pin mode, A1IE (alarm1 int enable)
  Wire.write(0b00000000);     // 0F Reset current alarm(s)
  Wire.endTransmission();
}


/// \brief Disable RTC alarms and clear interrupt (RTC_INT_PIN pin to HIGH)
void rtcDisableAlarm()
{
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write((byte) 0x0E);
  Wire.write(0b00000100);     // 0E INTCN pin in alarm mode, no alarms set
  Wire.write(0b00000000);     // 0F Reset current alarm(s)
  Wire.endTransmission();
}

//////////////////////////////////////////////////
// INTERRUPT CODE

const byte RTC_INT_PIN = 3; // must be 2 or 3
// NOTE: used for SPI clk, use other pin in final code
const byte LED_AWAKE_PIN = 13;    

volatile bool intTriggered = false;


void rtcIntHandler()
{
  intTriggered = true;
}


// NOTE:
// According to some sources LOW is only valid option for SLEEP_MODE_PWR_DOWN
// Problem is that program seems to get stuck in rtcIntHandler, but detaching
// interrupt may work ... have not tried.
void rtcIntSetup()
{
  pinMode(RTC_INT_PIN, INPUT_PULLUP);
  attachInterrupt (RTC_INT_PIN - 2, rtcIntHandler, FALLING);
}

///////////////////////////////////
// PERIODIC CODE

void sleepNow()
{
  // Wait for serial writes to complete
  delay(100);
  
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
  sleep_cpu();
}


void periodic() 
{
  digitalWrite(LED_AWAKE_PIN, HIGH);
  rtcAlarmInMinutes(1);
  Serial.print(rtcDateTimeStr());    
  Serial.print(", INT triggered\n");
  delay(500);
  digitalWrite(LED_AWAKE_PIN, LOW);
  sleepNow();  
}


//////////////////////////////////////////////////////////////////////////////////////////////////
// PROGRAM

void setup()
{
  pinMode(LED_AWAKE_PIN, OUTPUT);
  digitalWrite(LED_AWAKE_PIN, HIGH);
  Serial.begin(9600);
  while (!Serial)
    ;
  Serial.print("Boot up ...\n");
  Wire.begin();
  Serial.print("Disabling alarm ...\n");
  rtcDisableAlarm();
  Serial.print("Setting up alarm ...\n"); 
  // Sleep for two minutes, it's safe cause interrupt won't happen in next second if we run this at 59th second
  rtcAlarmInMinutes(2);
  Serial.print("Current time is ");
  Serial.println(rtcDateTimeStr());
  Serial.print("Setting up interrupt and going to sleep for minute or two ...\n"); 
  rtcIntSetup();
  Serial.flush();
  delay(1000);
  digitalWrite(LED_AWAKE_PIN, LOW);
  sleepNow();
}


void loop()
{
  if (intTriggered) {
    intTriggered = false;
    periodic();
  } else {
    // Should not be reached
    Serial.print(rtcDateTimeStr());
    Serial.println(", INT not triggered\n");
    delay(1000);
  }
}

ZS-042 Modifications and Current Draw

Added 2024-03-24, modified 2024-04-13

I tried to measure power draw using Voltcraft VC175 multimeter with VCC connected to 5V or 3.3V and I2C not connected. Disabled charging circuit means that diode and resistor were desoldered. Removed LED means that 1K resistor and LED were desoldered.

TODO: photo of before and after, link to data logger, mention INT pin pull-up resistor

Conclusion is that 5V really tries to charge non-rechargeable CR2032 battery, whereas with 3.3V battery is not recharged.

Is rechargeable battery needed? If I understand datasheet correctly, the worst case power draw is 3uA. That means 3uAh of battery capacity per hour, 72uAh/day, 2.2mAh/month, 26.4mAh/year, 210mAh in 8 years. This is the worst case scenario and it matches life time of CR-2032 battery.

Well … talking about worst case scenarios, battery capacity and voltage drops with temperature 🥶, and it’s specified by discharge to 2V which is not enough for RTC

Desoldering LED is likely impossible, it seems glued to the board and way I removed is was mechanical destruction by a screw driver. For now I’d call it good enough and VCC could be connected to some microcontroller output pin anyways.

VCCLEDBatteryCurrentNote
5.0PresentPresent6.74mABattery charging
3.3PresentPresent1800uA 
5.0PresentNot present3.92mA 
3.3PresentNot present1800uASame as with battery
5.0PresentCharging disabled3.92mA 
3.3PresentCharging disabled1800uA 
5.0RemovedCharging disabled820uAWhy? Cannot reproduce
3.3RemovedCharging disabled550uAWhy? Cannot reproduce
5.0RemovedCharging disabled130.5uA 
3.3RemovedCharging disabled90.5uA 

Weirdly two measurements are not confirmed later and power draw at 3.3V should be ~90uA. Is this because I left pins SDA/SCL pins floating during this measurement? Only way how to considerably increase power consumption on VCC is to ground SDA or SCL pin, current then goes through pull-up resistors. Maybe this was the case. Note that when SDA/SCL pins are connected to VCC, small current (1-2uA) can go through these pins and measurement on VCC can be slightly below 90uA.

There are some short current spikes, likely when RTC performs temperature calibration, but I’m unable to measure them using multimeter.

Closing words

By integrating the DS3231 module into your data logger project, you’ll be able to reliably record data points with accurate timestamps, whether you’re monitoring environmental conditions, tracking sensor readings, or logging any other time-sensitive data.

With the knowledge gained from this tutorial, you’re now equipped to incorporate the DS231 into your Arduino projects, unlocking a world of possibilities for building robust and efficient data logging systems. Whether you’re a hobbyist, a researcher, or an embedded systems developer, mastering the DS3231 will undoubtedly prove invaluable in your future endeavors.

Changelog

  • 2016-05-31 Major part of this article written: RTC interrupts, setting time
  • 2024-03-21 Introduction and Closing words kindly provided by Claude3 Opus AI model.
  • 2024-03-29 Info specific to ZS-042 module, (more) links
This post is licensed under CC BY 4.0 by the author.