RTC RV-3028 drifting 41x worse than spec

Hi Brains trust!

I have a Real Time Clock RV-3028 connected to WizNet W5500 Pico Board with PoE Hat running micropython. It is drifting at -0.13s / hr when powered, which is 41x worse than spec (actual is approx 20 min per year vs spec of approx 30s per year).

I have replaced all the parts and achieved a similar but slightly worse result of -0.15s / hr. I have tried 2 different test code bases and validated against 2 independent time sources. The I2C is not connected to anything else. In the replacement parts test there were no additional peripherals. The power supply is USB / PoE. Environment has stable room temperature.

My requirement is for a competition timing system with drift of less than 0.01s / hr.

Is the spec wrong / do I have a bad batch of RV-3028s / what alternative hardware could I consider / etc!

Thoughts most welcome Makerverse please!!

1 Like

Hi Ben,

Welcome to the forum and sorry to hear!

Could you please send through the code you are using? Have you charged the capacitor before your tests?

Thanks @Liam120347. Capacitors have been periodically charged. Yesterday test ran for 6hrs, powered throughout, with no significant improvement in performance.

"""

RTC Drift Tester - SIMPLE VERSION

Uses Pico's ticks_ms() as the timebase reference instead of repeated NTP queries

"""




import time

import network

import ntptime

from machine import Pin, I2C




# ============================================================================

# CONFIGURATION

# ============================================================================

STATIC_IP = "192.168.1.120"

SUBNET_MASK = "255.255.255.0"

GATEWAY = "192.168.1.1"

DNS_SERVER = "192.168.1.1"




NTP_SERVER = "au.pool.ntp.org"

TIMEZONE_OFFSET = 11 * 3600  # UTC+11 for AEDT




CHECK_INTERVAL = 60  # seconds between drift checks




# ============================================================================




# --- I2C setup for RV3028 ---

i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)

RV3028_ADDR = 0x52




# --- BCD conversion helpers ---

def bcd_to_int(b):

return (b >> 4) * 10 + (b & 0x0F)




def int_to_bcd(i):

return ((i // 10) << 4) | (i % 10)




def write_regs(start_addr, data):

i2c.writeto_mem(RV3028_ADDR, start_addr, bytes(data))




def read_regs(start_addr, nbytes):

return i2c.readfrom_mem(RV3028_ADDR, start_addr, nbytes)




# --- Ethernet connection ---

def ethernet_connect():

try:

nic = network.WIZNET5K()

nic.active(True)

nic.ifconfig((STATIC_IP, SUBNET_MASK, GATEWAY, DNS_SERVER))

print(f"Ethernet configured: {STATIC_IP}")

timeout = 10

while not nic.isconnected() and timeout > 0:

time.sleep(1)

timeout -= 1

print(".", end="")

if nic.isconnected():

print(f"\nEthernet connected: {nic.ifconfig()}")

return True

else:

print("\nEthernet connection failed")

return False

except Exception as e:

print(f"Ethernet error: {e}")

return False




# --- Read RV3028 as seconds since epoch ---

def read_rtc_datetime():

"""Read RV3028 and return (year, month, day, hour, minute, second)"""

data = read_regs(0x00, 7)

second = bcd_to_int(data[0] & 0x7F)

minute = bcd_to_int(data[1] & 0x7F)

hour = bcd_to_int(data[2] & 0x3F)

day = bcd_to_int(data[4] & 0x3F)

month = bcd_to_int(data[5] & 0x1F)

year = 2000 + bcd_to_int(data[6])

return (year, month, day, hour, minute, second)




# --- Sync RTC from NTP ---

def sync_rtc_from_ntp():

"""Sync RV3028 from NTP"""

try:

ntptime.host = NTP_SERVER

print(f"Syncing with NTP server: {NTP_SERVER}")

# Get NTP time

ntptime.settime()

# Get UTC time and convert to local

utc_timestamp = time.time()

local_timestamp = utc_timestamp + TIMEZONE_OFFSET

t = time.localtime(local_timestamp)

year = t[0] % 100

month, day, wday, hour, minute, second = (

t[1], t[2], (t[6] + 1) % 7, t[3], t[4], t[5]

        )

# Write to RV3028

data = [

int_to_bcd(second),

int_to_bcd(minute),

int_to_bcd(hour),

int_to_bcd(wday),

int_to_bcd(day),

int_to_bcd(month),

int_to_bcd(year),

        ]

write_regs(0x00, data)

print(f"RTC synced: 20{year:02d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d} (local)")

return True

except Exception as e:

print(f"NTP sync failed: {e}")

return False




# --- Format seconds as readable time ---

def format_drift_time(seconds):

"""Format drift in seconds as a readable string"""

abs_sec = abs(seconds)

if abs_sec < 1:

return f"{seconds*1000:.1f} ms"

elif abs_sec < 60:

return f"{seconds:.3f} s"

else:

minutes = abs_sec / 60

return f"{minutes:.2f} min ({seconds:.1f} s)"




# --- Main program ---

def main():

print("="*60)

print("RV3028 RTC Drift Tester (Simple Version)")

print("="*60)

if not ethernet_connect():

print("Cannot continue without network")

return

# Initial sync

print("\n--- Initial Sync ---")

if not sync_rtc_from_ntp():

print("Initial sync failed, cannot continue")

return

print("\nWaiting for stable second boundary...")

# Wait until we're at the start of a second for clean reference

last_sec = -1

while True:

dt = read_rtc_datetime()

if dt[5] != last_sec:  # Second changed

last_sec = dt[5]

# Wait for next second boundary

while read_rtc_datetime()[5] == last_sec:

time.sleep_ms(10)

# Now we're at a clean second boundary

break

# Capture reference point

ref_rtc_time = read_rtc_datetime()

ref_pico_ticks = time.ticks_ms()

print(f"\nReference captured at: {ref_rtc_time[0]}-{ref_rtc_time[1]:02d}-{ref_rtc_time[2]:02d} "

f"{ref_rtc_time[3]:02d}:{ref_rtc_time[4]:02d}:{ref_rtc_time[5]:02d}")

print("\n--- Starting Drift Monitoring ---")

print("Checking drift every", CHECK_INTERVAL, "seconds")

print("Using Pico's ticks_ms() as reference timebase")

print("Press Ctrl+C to stop\n")

print(f"{'Elapsed':<12} {'RTC Drift':<15} {'Drift Rate':<20}")

print("-" * 60)

try:

while True:

time.sleep(CHECK_INTERVAL)

# Read current RTC time

current_rtc = read_rtc_datetime()

current_pico_ticks = time.ticks_ms()

# Calculate elapsed time according to Pico (in seconds)

pico_elapsed_ms = time.ticks_diff(current_pico_ticks, ref_pico_ticks)

pico_elapsed_sec = pico_elapsed_ms / 1000.0

# Calculate elapsed time according to RTC (in seconds)

# Simple calculation: convert both times to total seconds since midnight

ref_total_sec = ref_rtc_time[3] * 3600 + ref_rtc_time[4] * 60 + ref_rtc_time[5]

current_total_sec = current_rtc[3] * 3600 + current_rtc[4] * 60 + current_rtc[5]

# Handle day rollover

if current_rtc[2] > ref_rtc_time[2]:

current_total_sec += 86400 * (current_rtc[2] - ref_rtc_time[2])

rtc_elapsed_sec = current_total_sec - ref_total_sec

# Calculate drift (RTC elapsed - Pico elapsed)

drift = rtc_elapsed_sec - pico_elapsed_sec

# Calculate drift rate (seconds per hour)

drift_rate = (drift / pico_elapsed_sec) * 3600 if pico_elapsed_sec > 0 else 0

# Format output

elapsed_str = f"{pico_elapsed_sec/60:.1f} min"

drift_str = format_drift_time(drift)

rate_str = f"{drift_rate:+.4f} s/hour"

print(f"{elapsed_str:<12} {drift_str:<15} {rate_str:<20}")

except KeyboardInterrupt:

print("\n\n--- Test Summary ---")

final_rtc = read_rtc_datetime()

final_pico_ticks = time.ticks_ms()

pico_elapsed_ms = time.ticks_diff(final_pico_ticks, ref_pico_ticks)

pico_elapsed_sec = pico_elapsed_ms / 1000.0

ref_total_sec = ref_rtc_time[3] * 3600 + ref_rtc_time[4] * 60 + ref_rtc_time[5]

final_total_sec = final_rtc[3] * 3600 + final_rtc[4] * 60 + final_rtc[5]

if final_rtc[2] > ref_rtc_time[2]:

final_total_sec += 86400 * (final_rtc[2] - ref_rtc_time[2])

rtc_elapsed_sec = final_total_sec - ref_total_sec

total_drift = rtc_elapsed_sec - pico_elapsed_sec

avg_drift_rate = (total_drift / pico_elapsed_sec) * 3600 if pico_elapsed_sec > 0 else 0

print(f"Total test duration: {pico_elapsed_sec/60:.1f} minutes ({pico_elapsed_sec/3600:.2f} hours)")

print(f"Total drift: {format_drift_time(total_drift)}")

print(f"Average drift rate: {avg_drift_rate:+.4f} seconds/hour")

print(f"Projected drift per day: {format_drift_time(avg_drift_rate * 24)}")

print(f"Projected drift per week: {format_drift_time(avg_drift_rate * 24 * 7)}")

print("\nTest stopped.")




# Run the test

main()

A few things come to mind when looking at this …

  1. NTP can have good and less then ideal implementations. A single NTP packet may not yield the exact time; a reliable network to the NTP server can get better results then a very busy connection. The NTP server itself can have processing overhead and delays that can affect the time values it logs and when it actually does something… eg. It can add the “leave time”, but the packet can get delayed in leaving.
    As such, the NTP time you get once will be OK to set the time, but checking back to a new NTP could give an error/different results - You said you only checked the NTP once, so this should be OK.

  2. The Pico clock could drift as well.
    “… Raspberry Pi Pico’s clock is good for many applications, with the built-in timer and the RTC (Real-Time Clock) being accurate to within a few seconds per day…”

Last time I played with and checked an actual clock chip, I used a GPS for my clock reference. If you can track the PPS offset to when you get the time, it can be very close, and if you use that to compare to after the first set it should be closer.

At the very least, if you can cross reference the Pico Clock with a clock you trust (PC ?) you should be able to see < 1 second difference just with your eye. So if the pico clock is drifting you will have confirmed its a bad reference. On the other hand if it looks to be very close after an hour or 2 then you have proven it could be good enough as a reference.

1 Like

@Michael99645 thank you for taking the time to consider this.

Yes finding a trusted time benchmark is a challenge. Over the past several months I have tested various, including GPS clock, NTP and several others. One of our pico accuracy tests was conducted using a professional sports timing system (not NTP). All the signs are that it is the pico RTC that is systematically drifting.