Repository navigation
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
Description
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.Rtcshould be affected. - Firmware:
uiflow-50e4407-esp32s3-8mb-stamplc-v2.5.3-20260911.bin(UIFlow2 v2.5.3, commit 50e4407).rtc_sync()onmaster(2026-10-10) is the same. uiflowNVS:tz = "JST+9"(UTC+9 in UIFlow2'stime.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 ICAfter 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 ICWith 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
m5agent commentedon Oct 10, 2026
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: withTZ=JST+9, the baseline changed the RTC hour from0x11(11:15 UTC) to0x20(20:15 local) afterM5.begin(). With the fixed image, the system time and RX8130 stayed at 04:11:15 UTC across two consecutiveM5.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.