The Raspberry Pi is a powerhouse for embedded projects, but it has a glaring hardware omission: no onboard CMOS battery. Every time you lose power, the Pi forgets the time, relying entirely on NTP servers once it reconnects to the network. If you are building an off-grid weather station, a data logger in a Faraday cage, or a retro-gaming console that needs accurate save timestamps, you need a dedicated hardware clock. The DS3231 is the undisputed standard for this job.
This guide covers the exact wiring, register-level Python code, and I2C debugging steps to get a Raspberry Pi real time clock running reliably. We are targeting the Raspberry Pi 4 Model B (4GB) and the Raspberry Pi 5 (via the standard 40-pin GPIO header I2C bus).
RTC Module Comparison & Parts List
Not all RTCs are created equal. The hobbyist market is flooded with cheap DS1307 modules that drift by minutes every month. For any project where timestamp accuracy matters, you must use a Temperature Compensated Crystal Oscillator (TCXO) chip.
| Chip / Module | Typical Accuracy | Temp Compensation | I2C Address | Avg. Price (USD) |
|---|---|---|---|---|
| DS3231 (ZS-042) | ±2 ppm (±1 min/year) | Yes (Internal TCXO) | 0x68 | $3.50 - $5.00 |
| DS1307 | ±20 ppm (±5 min/month) | No | 0x68 | $1.50 - $2.50 |
| PCF8523 | ±10 ppm | No | 0x68 | $3.00 - $4.00 |
| DS3232 | ±2 ppm | Yes (Internal TCXO) | 0x68 | $8.00 - $12.00 |
Required Parts
- Microcontroller: Raspberry Pi 4 Model B or Raspberry Pi 5 (Any RAM variant)
- RTC Module: DS3231 on a ZS-042 breakout board (includes 4.7kΩ pull-ups and an AT24C32 EEPROM)
- Battery: CR2032 3V Lithium Coin Cell (Do NOT use LIR2032 rechargeable cells on the ZS-042 without modifying the charging circuit, as it can overheat and vent)
- Wiring: 4x Female-to-Female Dupont jumper wires (22 AWG stranded)
Hardware Wiring & Pin Mapping
The DS3231 communicates over I2C, requiring only power and two data lines. The ZS-042 breakout board is designed for 5V logic but includes an onboard voltage regulator. It is perfectly safe to wire directly to the Pi's 3.3V pins, which is actually preferred to avoid back-feeding 5V into the Pi's I2C pull-ups.
| DS3231 Pin Label | Function | Raspberry Pi GPIO Pin (Physical) | Raspberry Pi Pin Name |
|---|---|---|---|
| GND | Ground Reference | Pin 6 | GND |
| VCC | Power Input (3.3V) | Pin 1 | 3V3 Power |
| SDA | I2C Data | Pin 3 | GPIO 2 (SDA1) |
| SCL | I2C Clock | Pin 5 | GPIO 3 (SCL1) |
Physical Connection Steps
- De-energize the Pi: Shut down the OS (
sudo shutdown -h now) and unplug the USB-C power supply. - Insert the Battery: Slide the CR2032 into the ZS-042 battery holder. Ensure the positive (+) side faces up.
- Wire Ground and Power: Connect DS3231 GND to Pi Pin 6, and DS3231 VCC to Pi Pin 1 (3.3V).
- Wire I2C Bus: Connect DS3231 SDA to Pi Pin 3, and DS3231 SCL to Pi Pin 5.
- Verify Connections: Give the Dupont wires a gentle tug. Loose crimps on cheap jumper wires are the #1 cause of intermittent I2C drops.
Software Configuration & Python Code
Before writing code, enable the I2C interface on the Pi. Run sudo raspi-config, navigate to Interface Options > I2C, and enable it. Reboot the Pi. For a deeper understanding of I2C bus configuration on Raspberry Pi OS, refer to the official Raspberry Pi I2C documentation.
We will use the smbus2 library to read and write directly to the DS3231 registers. This avoids heavy dependencies and teaches you how the Binary Coded Decimal (BCD) registers actually work. Install it via terminal: pip3 install smbus2.
Target Board Variant: Raspberry Pi 4 Model B / Pi 5 (I2C Bus 1).
import smbus2
import datetime
import sys
import time
# I2C Configuration
I2C_BUS = 1
RTC_ADDR = 0x68
def bcd_to_dec(bcd):
"""Convert Binary Coded Decimal to standard integer."""
return ((bcd >> 4) * 10) + (bcd & 0x0F)
def dec_to_bcd(dec):
"""Convert standard integer to Binary Coded Decimal."""
return ((dec // 10) << 4) | (dec % 10)
def read_rtc():
try:
bus = smbus2.SMBus(I2C_BUS)
# Read 7 bytes starting from register 0x00 (Seconds)
data = bus.read_i2c_block_data(RTC_ADDR, 0x00, 7)
sec = bcd_to_dec(data[0] & 0x7F)
min = bcd_to_dec(data[1])
hr = bcd_to_dec(data[2] & 0x3F)
day = bcd_to_dec(data[4])
month = bcd_to_dec(data[5] & 0x1F)
year = bcd_to_dec(data[6]) + 2000
dt = datetime.datetime(year, month, day, hr, min, sec)
print(f"RTC Time: {dt.strftime('%Y-%m-%d %H:%M:%S')}")
return dt
except FileNotFoundError as e:
print(f"CRITICAL: {e}")
print("Fix: I2C is not enabled. Run 'sudo raspi-config' and enable I2C.")
sys.exit(1)
except OSError as e:
print(f"HARDWARE ERROR: {e}")
print("Fix: Check wiring. Run 'i2cdetect -y 1' to verify address 0x68.")
sys.exit(1)
if __name__ == '__main__':
read_rtc()
fake-hwclock to save the time to a file on shutdown and restore it on boot. If you want the OS to sync strictly from your DS3231 on boot, you must disable this service: sudo systemctl disable fake-hwclock and configure hwclock in your /lib/udev/hwclock-set script.
Debugging I2C Failures
When working with bare I2C on the Pi, things will go wrong. Here are the exact error strings Python will throw, ranked by their most likely causes.
Error 1: OSError: [Errno 121] Remote I/O error
This is the most common I2C error. It means the Pi sent a request to address 0x68, but the DS3231 NACK'd (did not acknowledge) or the bus physically dropped the signal.
- Loose Dupont Connectors: The female headers on cheap jumper wires often stretch out. Swap the wires or crimp them tighter with a small flathead screwdriver.
- Address Collision / EEPROM Conflict: The ZS-042 board also has an AT24C32 EEPROM at address 0x57. If you accidentally scan or write to the wrong address, the bus can lock up.
- Missing Pull-up Resistors: The ZS-042 has 4.7kΩ pull-ups onboard. If you are using a bare DS3231 chip or a different breakout, you must add 4.7kΩ resistors between SDA/SCL and 3.3V.
Error 2: FileNotFoundError: [Errno 2] No such file or directory: '/dev/i2c-1'
The Python script cannot find the I2C device node in the Linux filesystem.
- I2C Interface Disabled: You forgot to enable I2C in
raspi-config. Enable it and reboot. - Wrong Bus Number: Older Pi revisions (Pi 1 Model B Rev 1) used I2C Bus 0. All modern Pis (Zero, 3, 4, 5) use Bus 1. Ensure
I2C_BUS = 1in the code.
Error 3: OSError: [Errno 110] Connection timed out
The Pi is waiting for the clock line to be pulled high, but it never happens. This is a 'clock stretching' failure.
- Bus Capacitance Too High: If your wires are longer than 30cm (1 foot), the capacitance of the wire prevents the 3.3V pull-ups from rising fast enough. Shorten the wires or use a dedicated I2C bus extender (like the PCA9600).
- 5V Back-feed: If you wired VCC to Pin 2 (5V) instead of Pin 1 (3.3V), the 5V pull-ups on the ZS-042 might be fighting the Pi's internal 3.3V protection diodes, causing logic level confusion. Stick to 3.3V.
The First Three Things to Check When It Fails
Before rewriting code, run this physical and logical triage:
- Run
i2cdetect -y 1: You should see68in the grid. If you seeUU, the kernel driver (rtc-ds1307) has already claimed the device. You must blacklist the driver or use the systemhwclockcommand instead of Python. - Measure VCC with a Multimeter: Put your probes on the GND and VCC pins of the breakout. You must read between 3.2V and 3.4V. If it reads 0V, your jumper wire is dead.
- Check
fake-hwclockStatus: Runsystemctl status fake-hwclock. If it's active, it might be overwriting your hardware clock with stale data on every reboot.
Extending and Simplifying the Build
Once your Raspberry Pi real time clock is stable, you can adapt the hardware to fit your specific project constraints.
How to Extend: Logging Ambient Temperature
The DS3231 isn't just a clock; it's a highly accurate thermometer. The chip uses its internal temperature readings to adjust the crystal oscillator, and it exposes this data in registers 0x11 (MSB) and 0x12 (LSB). By adding a few lines to the Python script, you can read the temperature in 0.25°C increments. This is invaluable for environmental data loggers where adding a separate I2C temperature sensor (like a BME280) would crowd the bus.
How to Simplify: HATs and Pi 5 Native Headers
If dealing with Dupont wires and I2C bus capacitance sounds like a headache, you have two simplification paths:
- For Pi 3 / Pi 4 Users: Buy an RTC HAT (like the Adafruit ChronoDot or generic DS3231 HATs). These plug directly into the 40-pin header, eliminating wiring errors entirely and often include a dedicated EEPROM and prototyping area.
- For Pi 5 Users: Skip the I2C module entirely. Purchase the official Raspberry Pi 5 RTC Battery (a CR2032 with a 2-pin JST-SH connector). Plug it directly into the J5 header on the Pi 5 board. The RP1 chip handles the rest natively via the
rtcoverlay in/boot/firmware/config.txt, requiring zero Python I2C code.
For the definitive electrical characteristics and register maps of the DS3231, always keep the Analog Devices DS3231 Datasheet bookmarked on your bench tablet. Understanding the raw hex registers is what separates a script-kiddie from an embedded engineer.






