Skip to content

M5.begin() writes local time into the RTC IC but the IC is read back as UTC, so the clock advances by the timezone offset on every reboot #117

Closed

Description

@wrpro

Environment

  • Board: M5Stack StamPLC (K141), RTC IC RX8130CE. The code path is board independent, so every board whose RTC is handled by M5Unified's M5.Rtc should be affected.
  • Firmware: uiflow-50e4407-esp32s3-8mb-stamplc-v2.5.3-20260911.bin (UIFlow2 v2.5.3, commit 50e4407). rtc_sync() on master (2026-10-10) is the same.
  • uiflow NVS: tz = "JST+9" (UTC+9 in UIFlow2's time.timezone() convention). time.localtime() is UTC+9 as expected.

Steps to reproduce

REPL on the StamPLC with Wi-Fi connected. rtc_regs() dumps RX8130CE registers 0x10..0x16 (sec, min, hour, weekday, day, month, year in BCD) through the internal I2C bus:

>>> from stamplc.plc import get_i2c
>>> def rtc_regs():
...     return ' '.join('%02x' % b for b in get_i2c().readfrom_mem(0x32, 0x10, 7))
...
>>> import time, ntptime, M5
>>> ntptime.settime()                 # sets system time (UTC) and writes it through to the RTC IC
>>> time.gmtime()[:6]
(2026, 10, 9, 17, 11, 15)
>>> rtc_regs()
'15 11 17 20 09 10 26'                # 2026-10-09 17:11:15 = UTC, consistent with the system time
>>> M5.begin()
>>> time.gmtime()[:6]
(2026, 10, 9, 17, 11, 15)             # system time unchanged: the IC was read as UTC
>>> rtc_regs()
'15 11 02 40 10 10 26'                # 2026-10-10 02:11:15 = local time (UTC+9) was written to the IC

After machine.reset(), M5.begin() (called from main.py) reads 02:11 back as UTC, so the system time is now 9 h ahead. Each reboot adds another 9 h. Three reboots a few seconds apart on the same device gave time.gmtime() = 08:23:18, 17:23:21, 02:23:24.

Cause

m5stack/components/M5Unified/mpy_m5unified.cpp:

void rtc_sync(struct timeval *tv_in) {
    struct timeval tv;
    if (tv_in == nullptr) {
        gettimeofday(&tv, NULL);
    } else {
        tv = *tv_in;
    }
    struct tm now;
    localtime_r(&tv.tv_sec, &now);   // <-- local time (TZ applied)
    M5.Rtc.setDateTime(&now);
}

mp_obj_t m5_begin(size_t n_args, const mp_obj_t *args) {
    ...
    M5.begin(cfg);                   // M5Unified: Rtc.setSystemTimeFromRtc() reads the IC as UTC
    ...
    // get local time and sync to RTC IC
    rtc_sync(nullptr);
    ...
}

M5Unified's RTC_Class::setSystemTimeFromRtc() forces TZ=GMT0 around mktime(), i.e. it interprets the IC content as UTC. So every M5.begin() does "system time = IC (as UTC)" followed by "IC = system time + tz". The net effect is +tz per boot. With the default tz = GMT0 the two cancel out, which is probably why it has gone unnoticed.

The other writer of the IC, machine.RTC().datetime(...) (used by ntptime.settime()), writes UTC, consistent with the read side. rtc_sync() is the only place that writes local time.

Expected

Rebooting must not change the time. The RTC IC should hold UTC consistently, matching M5Unified's read side and machine.RTC().datetime().

Proposed fix

Write UTC in rtc_sync():

     struct tm now;
-    localtime_r(&tv.tv_sec, &now);
+    gmtime_r(&tv.tv_sec, &now);   // the RTC IC keeps UTC; setSystemTimeFromRtc() reads it as UTC
     M5.Rtc.setDateTime(&now);

Alternatively drop the rtc_sync(nullptr) call from m5_begin(): the system time has just been loaded from the IC, so writing it back only has an effect when the two differ, which is exactly the bug.

Workaround used meanwhile

Right after M5.begin(), write the system time back to the IC in UTC through machine.RTC().datetime():

import machine
rtc = machine.RTC()
rtc.datetime(rtc.datetime())   # the firmware writes this (UTC) through to the RTC IC

With this in setup(), the clock survived two reboots without NTP within 1 s of the PC clock.

Related, not part of this issue

setSystemTimeFromRtc() adopts the IC content even when the IC's data-loss flag is set (RX8130CE VLF, register 0x1D bit 1). After a backup-battery brownout we saw the year 2052 become the system time. User code can check the flag itself, but a check in M5Unified might be worth a separate issue there.

Activity

  1. m5agent commented on Oct 10, 2026

    @m5agent
    Contributor

    Thanks for the detailed analysis. I fixed rtc_sync() to write UTC and opened #119.

    I reproduced the behavior on a StopWatch with RX8130, which uses the same M5.Rtc / rtc_sync() path: with TZ=JST+9, the baseline changed the RTC hour from 0x11 (11:15 UTC) to 0x20 (20:15 local) after M5.begin(). With the fixed image, the system time and RX8130 stayed at 04:11:15 UTC across two consecutive M5.begin() calls. The original StopWatch Wi-Fi image was restored and its application-partition prefix was verified against the stock artifact.

    Verification record:
    https://github.com/m5agent/agent-work-logs/blob/6367ac3cf17d3978ae18ca3aebf0c633b8f76b32/records/m5stack-github/2026-10-10-uiflow-issue-117-rtc-utc/README.md

    Limits: no StamPLC slot was available, so the hardware test used StopWatch; long-term battery-backup behavior was not tested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions