Exploring the DS3231 RTC with ESPHome: Setting Time, | LiberIT
![Exploring the DS3231 RTC with ESPHome: Setting Time, | LiberIT](https://liberit.ca/hfoc/Electronics-2024-12-30-IMG_20241230_182639.jpg)
Exploring the DS3231 RTC with ESPHome: Setting Time, Reading Time, and Alarms
The DS3231 Real-Time Clock (RTC) module is a versatile and accurate component that can help you manage timekeeping in your microcontroller projects. In this blog post, we’ll walk through what we learned today about using the DS3231 RTC with ESPHome, including setting the time, reading the time, and configuring alarms.
1. Setting the Time on the DS3231 RTC
When first using the DS3231 RTC module, you may need to set the current time so that the RTC can maintain accurate timekeeping. This step is essential for projects requiring precise timing, such as scheduling or logging.
Key Steps
- The time is written to the DS3231 by sending the year, month, day, hour, minute, and second to its registers over I²C.
- Here’s the method we used to set the time:
void set_time(int year, int month, int day, int hour, int minute, int second) { Wire.beginTransmission(0x68); Wire.write(0); // Start at register 0 Wire.write(dec_to_bcd(second)); Wire.write(dec_to_bcd(minute)); Wire.write(dec_to_bcd(hour)); Wire.write(dec_to_bcd(day)); Wire.write(dec_to_bcd(month)); Wire.write(dec_to_bcd(year - 2000)); Wire.endTransmission(); }
- Once set, the DS3231 maintains the time even when powered down, thanks to its onboard battery.
Lessons Learned
- Always verify the time set on the DS3231 by reading it back (see the next section).
- Use a precise time source, such as SNTP, to ensure accuracy when setting the time.
2. Reading the Time from the DS3231 RTC
Reading the current time from the DS3231 is straightforward. The time registers can be queried over I²C, and the values are converted from Binary-Coded Decimal (BCD) format to human-readable time.
Key Steps
- The time is read from the DS3231 by accessing its registers and decoding the values.
- Here’s the method we used to read the time:
Time get_time() { Wire.beginTransmission(0x68); Wire.write(0); // Start at register 0 Wire.endTransmission(); if (Wire.requestFrom(0x68, 7) != 7) { ESP_LOGE("DS3231", "Failed to read time registers"); return Time{0, 0, 0, 0, 0, 0}; } uint8_t second = bcd_to_dec(Wire.read()); uint8_t minute = bcd_to_dec(Wire.read()); uint8_t hour = bcd_to_dec(Wire.read()); Wire.read(); // Skip day-of-week uint8_t day = bcd_to_dec(Wire.read()); uint8_t month = bcd_to_dec(Wire.read()); uint16_t year = bcd_to_dec(Wire.read()) + 2000; return Time{year, month, day, hour, minute, second}; }
Lessons Learned
- Always check the validity of the time read from the DS3231. A default time of
2000-01-01
may indicate the RTC hasn’t been set. - Logs can be a helpful debugging tool to verify the time being read:
[I][DS3231]: Current RTC Time: 2024-12-30 16:31:00
3. Configuring Alarms on the DS3231 RTC
The DS3231 features two alarms (Alarm1 and Alarm2) that can trigger based on specific time conditions. These alarms are useful for periodic wakeups or notifications.
Configuring Alarm1
We configured Alarm1 to trigger at a specific second past every minute:
void set_alarm() {
Wire.beginTransmission(0x68);
Wire.write(0x07); // Start at Alarm1 seconds register
Wire.write(0x06); // Match exactly 6 seconds
Wire.write(0x80); // Match any minute
Wire.write(0x80); // Match any hour
Wire.write(0x80); // Match any day
Wire.endTransmission();
Wire.beginTransmission(0x68);
Wire.write(0x0E); // Control register
Wire.write(0x05); // Enable Alarm1 interrupt
Wire.endTransmission();
}
Lessons Learned
- Alarms trigger the SQW/INT pin, which can be used as an interrupt to wake up a microcontroller from deep sleep.
- The DS3231 alarm doesn’t natively support recurring intervals like "every 6 seconds." However, you can manually update the alarm after each trigger to achieve this.
Example Log Output
When the alarm triggers:
[I][DS3231]: Alarm Triggered!
4. Observations and Challenges
- Alarm Behavior: Setting the alarm to trigger at specific intervals (like every 6 seconds) required additional logic. By default, alarms work best for specific times or daily schedules.
- Deep Sleep Integration: The SQW/INT pin can wake the ESP32-C3 from deep sleep, making the DS3231 an excellent choice for low-power applications.
- Debugging: Logging the status registers and verifying pin behavior helped us debug alarm configurations effectively.
Conclusion
Today’s exploration of the DS3231 RTC with ESPHome provided a comprehensive look at its capabilities. From setting and reading time to configuring alarms, we learned how to leverage this module for accurate timekeeping and periodic wakeups. The DS3231 is a reliable and versatile component for projects requiring precise timing. I've also shared my results with github open issue for adding support for this chip to esphome on Github
Have you used the DS3231 RTC in your projects? Share your experiences and insights by contacting us!
Appendix 1: Setting code:
esphome:
name: ds3231-set-time
esp32:
board: esp32-c3-devkitm-1
framework:
type: arduino
i2c:
sda: GPIO20
scl: GPIO21
scan: true
logger:
wifi:
output_power: 8.5dB
networks:
- ssid: "yourWifiNetwork"
password: "yourPassword"
time:
- platform: sntp
id: sntp_time
timezone: America/Toronto
custom_component:
- lambda: |-
class DS3231Component : public PollingComponent {
public:
DS3231Component(esphome::time::RealTimeClock *rtc) : PollingComponent(10000), rtc_(rtc) {}
void setup() override {
if (!Wire.requestFrom(0x68, 1)) {
ESP_LOGE("DS3231", "RTC not found at I2C address 0x68");
return;
}
ESP_LOGI("DS3231", "RTC found at I2C address 0x68");
auto now = this->rtc_->now();
if (now.is_valid()) {
set_time(now.year, now.month, now.day_of_month, now.hour, now.minute, now.second, now.day_of_week);
ESP_LOGI("DS3231", "RTC time set to %04d-%02d-%02d %02d:%02d:%02d",
now.year, now.month, now.day_of_month, now.hour, now.minute, now.second);
} else {
ESP_LOGE("DS3231", "SNTP time is not valid; cannot set RTC");
}
}
void update() override {
auto time = get_time();
if (time.year > 0) {
ESP_LOGI("DS3231", "Current RTC Time: %04d-%02d-%02d %02d:%02d:%02d",
time.year, time.month, time.day, time.hour, time.minute, time.second);
} else {
ESP_LOGE("DS3231", "Failed to read time from RTC");
}
}
void set_time(int year, int month, int day, int hour, int minute, int second, int day_of_week) {
Wire.beginTransmission(0x68);
Wire.write(0); // Start at register 0
Wire.write(dec_to_bcd(second)); // Seconds
Wire.write(dec_to_bcd(minute)); // Minutes
Wire.write(dec_to_bcd(hour)); // Hours
Wire.write(dec_to_bcd(day_of_week)); // Day of the week
Wire.write(dec_to_bcd(day)); // Day of the month
Wire.write(dec_to_bcd(month)); // Month
Wire.write(dec_to_bcd(year - 2000)); // Year
Wire.endTransmission();
}
struct Time {
int year;
int month;
int day;
int hour;
int minute;
int second;
int day_of_week;
};
Time get_time() {
Wire.beginTransmission(0x68);
Wire.write(0); // Start at register 0
Wire.endTransmission();
if (Wire.requestFrom(0x68, 7) != 7) {
ESP_LOGE("DS3231", "Failed to read time registers");
return Time{0, 0, 0, 0, 0, 0, 0};
}
uint8_t second = bcd_to_dec(Wire.read());
uint8_t minute = bcd_to_dec(Wire.read());
uint8_t hour = bcd_to_dec(Wire.read());
uint8_t day_of_week = bcd_to_dec(Wire.read());
uint8_t day = bcd_to_dec(Wire.read());
uint8_t month = bcd_to_dec(Wire.read());
uint16_t year = bcd_to_dec(Wire.read()) + 2000;
return Time{year, month, day, hour, minute, second, day_of_week};
}
private:
esphome::time::RealTimeClock *rtc_;
uint8_t dec_to_bcd(int val) {
return ((val / 10 * 16) + (val % 10));
}
int bcd_to_dec(uint8_t val) {
return ((val / 16 * 10) + (val % 16));
}
};
auto my_rtc = new DS3231Component(id(sntp_time));
App.register_component(my_rtc);
return {};
Appendix 2: Reading code
esphome:
name: ds3231-read-time
platform: ESP32
board: esp32-c3-devkitm-1
i2c:
sda: GPIO20 # Adjust to your setup
scl: GPIO21
scan: true
logger:
# Use the DS1307 platform to interact with the DS3231 RTC
time:
- platform: ds1307
id: rtc_time
timezone: America/Toronto
on_time_sync:
then:
- logger.log: "Time synchronized with RTC."
- lambda: |-
auto now = id(rtc_time).now();
ESP_LOGI("RTC", "Current time: %04d-%02d-%02d %02d:%02d:%02d",
now.year, now.month, now.day_of_month, now.hour, now.minute, now.second);
interval:
- interval: 5s
then:
- lambda: |-
auto now = id(rtc_time).now();
ESP_LOGI("Time", "Current time: %04d-%02d-%02d %02d:%02d:%02d",
now.year, now.month, now.day_of_month, now.hour, now.minute, now.second);
Appendix 3: set alarm code
esphome:
name: ds3231-alarm-test
platform: ESP32
board: esp32-c3-devkitm-1
i2c:
sda: GPIO20 # Adjust to your wiring
scl: GPIO21
scan: true
logger:
binary_sensor:
- platform: gpio
pin:
number: GPIO9 # Pin connected to SQW/INT from DS3231
mode: INPUT_PULLUP
name: "DS3231 Alarm Trigger"
filters:
- delayed_off: 10ms # Debounce to avoid false positives
on_press:
- logger.log: "Alarm Detected!" # Log when interrupt occurs
custom_component:
- lambda: |-
class DS3231Alarm : public Component {
public:
void setup() override {
if (!Wire.requestFrom(0x68, 1)) {
ESP_LOGE("DS3231", "RTC not found at I2C address 0x68");
return;
}
ESP_LOGI("DS3231", "RTC found at I2C address 0x68");
// Set up the alarm to trigger every 6 seconds
set_alarm();
}
void set_alarm() {
Wire.beginTransmission(0x68);
Wire.write(0x07); // Start at Alarm1 seconds register
Wire.write(0x01); // A1M1=0: Match on first second
Wire.write(0x80); // A1M2=1: Match any minute
Wire.write(0x80); // A1M3=1: Match any hour
Wire.write(0x80); // A1M4=1: Match any day
Wire.endTransmission();
// Enable Alarm1 interrupt and disable square wave
Wire.beginTransmission(0x68);
Wire.write(0x0E); // Control register
Wire.write(0x05); // INTCN=1 (enable interrupt), A1IE=1 (enable Alarm1)
Wire.endTransmission();
ESP_LOGI("DS3231", "Alarm set to trigger every 6 seconds.");
}
void loop() override {
// Check if Alarm1 triggered by reading the status register
Wire.beginTransmission(0x68);
Wire.write(0x0F); // Status register
Wire.endTransmission();
Wire.requestFrom(0x68, 1);
uint8_t status = Wire.read();
if (status & 0x01) { // Alarm1 flag (A1F) is set
ESP_LOGI("DS3231", "Alarm Triggered!");
// Clear the Alarm1 flag
Wire.beginTransmission(0x68);
Wire.write(0x0F); // Status register
Wire.write(status & ~0x01); // Clear A1F
Wire.endTransmission();
}
}
};
auto my_alarm = new DS3231Alarm();
App.register_component(my_alarm);
return {};