I2C
I2C (Inter-Integrated Circuit, pronounced "I-squared-C") is used by the DS3231 real-time clock. It is slower than SPI but requires only two wires and supports many devices on the same bus.
The Two Wires
| Signal | Direction | Purpose |
|---|---|---|
| SDA | Bidirectional | Serial Data — carries both address and data bytes |
| SCL | Controller → Peripheral | Serial Clock — ESP32 drives this |
Both wires require pull-up resistors to VCC. Most breakout boards (including the DS3231 module) include them. The typical value is 4.7 kΩ. For a deeper explanation of why I2C needs external pull-ups and how to choose values, see Essential Components: Pull-Up Resistors.
Device Addressing
Every I2C device has a 7-bit address. Multiple devices share SDA and SCL; the controller addresses a specific device by sending its address at the start of every transaction.
The DS3231's default address is 0x68. If you later add an I2C device with the same address, you need to either change one address via a hardware pad or use a second I2C bus.
The software analogy: I2C is like an HTTP API where each device is a microservice with its own hostname (address) on a private network (the bus). You cannot have two services at the same address.
Open-Drain Bus
Both SDA and SCL are open-drain lines: any device can pull them LOW by connecting them to GND internally, but no device can drive them HIGH directly. Instead, the pull-up resistors passively hold the line HIGH when nothing is pulling it LOW.
This is why a missing pull-up resistor causes I2C to fail silently — the bus is stuck LOW or the clock never rises. Always verify pull-ups are present.
Finding Your DS3231
Before writing firmware, run an I2C scanner sketch to confirm the DS3231 appears at 0x68:
#include "driver/i2c.h"
void i2c_scan(void) {
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = GPIO_NUM_21,
.scl_io_num = GPIO_NUM_22,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = 100000, // 100 kHz
};
i2c_param_config(I2C_NUM_0, &conf);
i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0);
printf("Scanning I2C bus...\n");
for (uint8_t addr = 1; addr < 127; addr++) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, pdMS_TO_TICKS(10));
i2c_cmd_link_delete(cmd);
if (ret == ESP_OK) {
printf(" Found device at 0x%02X\n", addr);
}
}
}
Expected output when the DS3231 is wired correctly:
Scanning I2C bus...
Found device at 0x68
Clock Speed
I2C supports multiple speeds:
- Standard mode: 100 kHz — use for bring-up and if you see reliability issues
- Fast mode: 400 kHz — DS3231 supports this; use for production
The DS3231 is read infrequently (once per second at most) so clock speed does not matter much in practice.
I2C in ESP-IDF v5
ESP-IDF v5 introduced a new I2C master API. Use i2c_master_bus_add_device to register
the DS3231:
i2c_master_bus_config_t bus_cfg = {
.i2c_port = I2C_NUM_0,
.sda_io_num = GPIO_NUM_21,
.scl_io_num = GPIO_NUM_22,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_master_bus_handle_t bus;
i2c_new_master_bus(&bus_cfg, &bus);
i2c_device_config_t ds3231_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = 0x68,
.scl_speed_hz = 100000,
};
i2c_master_dev_handle_t ds3231;
i2c_master_bus_add_device(bus, &ds3231_cfg, &ds3231);
The RTC driver doc shows how to use this handle to read and write time registers.