From 01fd56a573bc7baf0f94d4ed71492d55793a540d Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:49:35 +0200 Subject: [PATCH] wire up RTC to out-of-the-box capable nodes --- zephcore/CMakeLists.txt | 6 + zephcore/Kconfig | 14 + zephcore/adapters/clock/ZephyrRTCDiscover.c | 243 ++++++++++++++++++ zephcore/adapters/clock/ZephyrRTCDiscover.h | 43 ++++ zephcore/app/CompanionMesh.cpp | 2 + zephcore/boards/common/rtc-i2c.dtsi | 69 +++++ zephcore/boards/common/sensors-i2c.dtsi | 5 + .../nrf52840/lilygo_techo/lilygo_techo.dts | 3 + .../nrf52840/thinknode_m1/thinknode_m1.dts | 3 + .../nrf52840/thinknode_m3/thinknode_m3.dts | 3 + .../nrf52840/thinknode_m6/thinknode_m6.dts | 3 + .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 43 ++++ zephcore/helpers/CommonCLI.cpp | 3 + zephcore/src/main_companion.cpp | 12 + zephcore/src/main_repeater.cpp | 11 + zephcore/src/main_room_server.cpp | 11 + 16 files changed, 474 insertions(+) create mode 100644 zephcore/adapters/clock/ZephyrRTCDiscover.c create mode 100644 zephcore/adapters/clock/ZephyrRTCDiscover.h create mode 100644 zephcore/boards/common/rtc-i2c.dtsi create mode 100644 zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index d30df1a..a36b676 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -444,6 +444,12 @@ target_sources(app PRIVATE helpers/fatal_reboot.c ) +# Boot-time hardware-RTC auto-discovery (compact raw-I2C). Always compiled so +# the zephcore_rtc_* symbols exist on every board; it self-stubs internally +# when CONFIG_ZEPHCORE_RTC_AUTODISCOVER is off or no zephcore,rtc-i2c node is +# present in DT. +target_sources(app PRIVATE adapters/clock/ZephyrRTCDiscover.c) + # ========== Battery Curve Selection ========== # helpers/battery_curve.c is always compiled — it provides battery_curve_lookup() # and the weak battery_curve_default. A board-specific battery_curve.c is compiled diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 3be2266..42217ce 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -523,6 +523,20 @@ config ZEPHCORE_APC endmenu +config ZEPHCORE_RTC_AUTODISCOVER + bool "Auto-discover a hardware I2C RTC at boot" + default y + depends on I2C + help + Probe I2C RTC chips declared with the "zephcore,rtc-i2c" binding + (DS3231 / PCF8563 / RV3028 / RX8130CE). Boards with a battery/cap- + backed RTC opt in by including boards/common/rtc-i2c.dtsi; if a chip + holds a valid time it is restored at boot — shown tagged "L" until the + next external sync. On every GPS/app/CLI sync the time is written back + so it survives power-off. Compact raw-I2C reader (no Zephyr RTC + subsystem). Safe to leave on: a board with no rtc-i2c.dtsi include has + no RTC nodes and compiles to nothing. + menu "GPS Configuration" config ZEPHCORE_GPS_POLL_INTERVAL_SEC diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c new file mode 100644 index 0000000..7471bbc --- /dev/null +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -0,0 +1,243 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Compact raw-I2C hardware-RTC auto-discovery. See ZephyrRTCDiscover.h. + * + * Register layouts (sec/min/hour/.../month/year, all BCD) and the per-chip + * power-loss flags are carried in devicetree via the "zephcore,rtc-i2c" + * binding, so this reader is generic — adding a new chip is a DT node, not + * code. Maps were taken from Zephyr's own drivers (rtc_pcf8563.c, + * rtc_ds3231.c, rtc_rv3028.c, rtc_rx8130ce.c). + */ + +#include "ZephyrRTCDiscover.h" + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(zephcore_rtc, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); + +#define RTC_COMPAT zephcore_rtc_i2c + +#if IS_ENABLED(CONFIG_ZEPHCORE_RTC_AUTODISCOVER) && DT_HAS_COMPAT_STATUS_OKAY(RTC_COMPAT) + +/* Validity flag lives in the seconds byte itself (PCF8563 VL bit). */ +#define RTC_STATUS_IN_SECONDS 0xFF + +struct rtc_desc { + const struct device *bus; + uint16_t addr; + uint8_t time_reg; /* register of the seconds byte */ + uint8_t date_index; /* day-of-month offset in the 7-byte block */ + uint8_t status_reg; /* power-loss flag register, or RTC_STATUS_IN_SECONDS */ + uint8_t status_mask; /* "time unreliable" bit within status_reg */ + const char *name; +}; + +#define RTC_DESC_ENTRY(node) \ + { \ + .bus = DEVICE_DT_GET(DT_BUS(node)), \ + .addr = (uint16_t)DT_REG_ADDR(node), \ + .time_reg = (uint8_t)DT_PROP(node, time_reg), \ + .date_index = (uint8_t)DT_PROP(node, date_index), \ + .status_reg = (uint8_t)DT_PROP(node, status_reg), \ + .status_mask = (uint8_t)DT_PROP(node, status_mask), \ + .name = DT_NODE_FULL_NAME(node), \ + }, + +static const struct rtc_desc rtc_descs[] = { + DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_DESC_ENTRY) +}; + +/* Chip we'll read/write going forward (first one found present). */ +static const struct rtc_desc *s_active; +static bool s_probed; + +#define BCD2BIN(x) ((((x) >> 4) & 0x0F) * 10 + ((x) & 0x0F)) +#define BIN2BCD(x) ((((x) / 10) << 4) | ((x) % 10)) + +/* A byte is valid BCD if both nibbles are 0-9, and its decoded value fits the + * field. Used to tell a real RTC apart from an unrelated I2C chip that happens + * to share an address (e.g. an MPU-class IMU at 0x68, same as DS3231) — we must + * never adopt and write time into such a device. */ +static bool bcd_field_ok(uint8_t v, unsigned max) +{ + if ((v & 0x0F) > 9 || (v >> 4) > 9) { + return false; + } + return BCD2BIN(v) <= max; +} + +/* Howard Hinnant's civil<->days algorithms (proleptic Gregorian, UTC). */ +static int64_t days_from_civil(int y, unsigned m, unsigned d) +{ + y -= (m <= 2); + int64_t era = (y >= 0 ? y : y - 399) / 400; + unsigned yoe = (unsigned)(y - era * 400); + unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + (int)doe - 719468; +} + +static void civil_from_days(int64_t z, int *y, unsigned *m, unsigned *d) +{ + z += 719468; + int64_t era = (z >= 0 ? z : z - 146096) / 146097; + unsigned doe = (unsigned)(z - era * 146097); + unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + int yy = (int)yoe + (int)(era * 400); + unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + unsigned mp = (5 * doy + 2) / 153; + *d = doy - (153 * mp + 2) / 5 + 1; + *m = mp < 10 ? mp + 3 : mp - 9; + *y = yy + (*m <= 2); +} + +/* Read the chip's power-loss flag. true => held time is unreliable. */ +static bool rtc_time_unreliable(const struct rtc_desc *d, const uint8_t blk[7]) +{ + if (d->status_reg == RTC_STATUS_IN_SECONDS) { + return (blk[0] & d->status_mask) != 0; + } + + uint8_t st; + if (i2c_reg_read_byte(d->bus, d->addr, d->status_reg, &st) != 0) { + return true; /* can't confirm => don't trust it */ + } + return (st & d->status_mask) != 0; +} + +/* Probe all chips once; cache the first present one in s_active. If a present + * chip holds a sane time, return it via epoch_out. */ +static bool rtc_probe(uint32_t *epoch_out) +{ + for (size_t i = 0; i < ARRAY_SIZE(rtc_descs); i++) { + const struct rtc_desc *d = &rtc_descs[i]; + uint8_t blk[7]; + + if (!device_is_ready(d->bus)) { + continue; + } + if (i2c_burst_read(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) != 0) { + continue; /* no ACK => chip absent */ + } + + /* Mask off flag/century bits, then require the block to be valid + * BCD. This is what proves a real RTC lives here (vs. an unrelated + * chip sharing the address) — only then is it safe to adopt and + * later WRITE to. A factory-fresh RTC reads a clean 2000-01-01, + * which is valid BCD (just stale), so it's still adopted. */ + uint8_t sb = blk[0] & 0x7F, mb = blk[1] & 0x7F, hb = blk[2] & 0x3F; + uint8_t db = blk[d->date_index] & 0x3F, ob = blk[5] & 0x1F, yb = blk[6]; + + if (!bcd_field_ok(sb, 59) || !bcd_field_ok(mb, 59) || + !bcd_field_ok(hb, 23) || !bcd_field_ok(db, 31) || + !bcd_field_ok(ob, 12) || !bcd_field_ok(yb, 99) || + BCD2BIN(db) < 1 || BCD2BIN(ob) < 1) { + continue; /* not a real RTC at this address — do not adopt */ + } + + if (s_active == NULL) { + s_active = d; /* confirmed RTC => our write-back target */ + } + + if (rtc_time_unreliable(d, blk)) { + LOG_WRN("%s present but time flagged unreliable", d->name); + continue; + } + + unsigned sec = BCD2BIN(sb); + unsigned min = BCD2BIN(mb); + unsigned hour = BCD2BIN(hb); + unsigned day = BCD2BIN(db); + unsigned month = BCD2BIN(ob); + unsigned year = 2000 + BCD2BIN(yb); + + if (year < 2025) { + LOG_INF("%s present, time not yet set (%04u-%02u-%02u)", + d->name, year, month, day); + continue; /* RTC adopted for write-back, but no valid time */ + } + + int64_t e = days_from_civil((int)year, month, day) * 86400LL + + hour * 3600 + min * 60 + sec; + if (epoch_out) { + *epoch_out = (uint32_t)e; + } + LOG_INF("RTC %s: restored %04u-%02u-%02u %02u:%02u:%02u UTC", + d->name, year, month, day, hour, min, sec); + return true; + } + return false; +} + +bool zephcore_rtc_restore(uint32_t *epoch_out) +{ + s_probed = true; + return rtc_probe(epoch_out); +} + +void zephcore_rtc_save(uint32_t epoch) +{ + if (!s_probed) { + /* Restore wasn't run (unexpected) — discover now. */ + (void)rtc_probe(NULL); + s_probed = true; + } + if (s_active == NULL) { + return; + } + + const struct rtc_desc *d = s_active; + int y; + unsigned m, day; + civil_from_days((int64_t)epoch / 86400, &y, &m, &day); + unsigned rem = epoch % 86400; + unsigned hour = rem / 3600; + unsigned min = (rem % 3600) / 60; + unsigned sec = rem % 60; + unsigned dow = (unsigned)(((epoch / 86400) + 4) % 7); /* 1970-01-01 = Thu */ + + uint8_t blk[7]; + blk[0] = BIN2BCD(sec); + blk[1] = BIN2BCD(min); + blk[2] = BIN2BCD(hour); + /* weekday occupies whichever of index 3/4 the date doesn't. */ + blk[d->date_index] = BIN2BCD(day); + blk[d->date_index == 4 ? 3 : 4] = (uint8_t)dow; + blk[5] = BIN2BCD(m); + blk[6] = BIN2BCD((unsigned)(y % 100)); + + if (i2c_burst_write(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) != 0) { + LOG_WRN("RTC %s: time write failed", d->name); + return; + } + + /* Clear the power-loss flag (for chips whose flag is a separate reg; + * the seconds-bit chips clear it implicitly when we wrote sec above). */ + if (d->status_reg != RTC_STATUS_IN_SECONDS) { + uint8_t st; + if (i2c_reg_read_byte(d->bus, d->addr, d->status_reg, &st) == 0) { + (void)i2c_reg_write_byte(d->bus, d->addr, d->status_reg, + st & (uint8_t)~d->status_mask); + } + } + LOG_DBG("RTC %s: persisted time", d->name); +} + +#else /* no zephcore,rtc-i2c node in DT — link-compatible stubs */ + +bool zephcore_rtc_restore(uint32_t *epoch_out) +{ + ARG_UNUSED(epoch_out); + return false; +} + +void zephcore_rtc_save(uint32_t epoch) +{ + ARG_UNUSED(epoch); +} + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(zephcore_rtc_i2c) */ diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h new file mode 100644 index 0000000..5af74da --- /dev/null +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Boot-time hardware-RTC auto-discovery (compact raw-I2C reader). + * + * Probes every I2C RTC chip declared with the "zephcore,rtc-i2c" binding + * (boards/common/rtc-i2c.dtsi, opt-in per board). Chips that aren't physically + * present fail the probe and are skipped — like the environment sensors. + * + * If a present chip holds a valid time, zephcore_rtc_restore() returns it so + * the soft clock can be seeded at boot (shown tagged "L" — local). On every + * authoritative sync (GPS/app/CLI) the caller writes it back via + * zephcore_rtc_save() so time survives the next power-off. + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Probe all declared RTC chips. If one is present and holds a sane time + * (year >= 2025 and its power-loss flag is clear), store the Unix epoch in + * *epoch_out and return true. The present chip (valid time or not) is + * remembered as the write-back target. Returns false if none present or no + * trustworthy time is held. + */ +bool zephcore_rtc_restore(uint32_t *epoch_out); + +/* + * Persist an authoritative epoch to the discovered RTC chip and clear its + * power-loss flag. No-op if no RTC was discovered. Safe to call often, but + * intended only for real syncs (GPS/app/CLI), not per-packet clock nudges. + */ +void zephcore_rtc_save(uint32_t epoch); + +#ifdef __cplusplus +} +#endif diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index e172ca9..84ca8c2 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_BUTTON) || IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK) #include #define ZEPHCORE_HAS_UI_TASK 1 @@ -2208,6 +2209,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) if (secs >= curr) { getRTCClock()->setCurrentTime(secs); time_sync_report(TIME_SYNC_APP); + zephcore_rtc_save(secs); /* persist to hardware RTC */ sendPacketOk(); } else { sendPacketError(ERR_ILLEGAL_ARG); diff --git a/zephcore/boards/common/rtc-i2c.dtsi b/zephcore/boards/common/rtc-i2c.dtsi new file mode 100644 index 0000000..3d1d9fe --- /dev/null +++ b/zephcore/boards/common/rtc-i2c.dtsi @@ -0,0 +1,69 @@ +/* + * Common I2C Real-Time-Clock descriptors for ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * OPT-IN: include this inside an &i2cN { ... } block ONLY on boards that + * physically have a battery/cap-backed RTC. Kept separate from + * sensors-i2c.dtsi because these declare fixed I2C addresses (0x68/0x51/ + * 0x52/0x32) that collide with common parts — an IMU/accelerometer sits at + * 0x68 (same as DS3231). Including this on a board without an RTC would risk + * a devicetree unit-address clash and a runtime mis-probe, so it is opt-in. + * + * Usage (board overlay/dts, alongside the sensors include): + * &i2c0 { + * #include "../../common/sensors-i2c.dtsi" + * #include "../../common/rtc-i2c.dtsi" + * }; + * + * These bind NO Zephyr RTC driver — they're data-only descriptors the + * boot-time auto-discovery (adapters/clock/ZephyrRTCDiscover.c) probes via + * raw I2C to restore wall-clock time at boot. A chip that isn't actually + * present fails the probe and is skipped. One chip per I2C address: + * + * Chip │ Addr │ Also covers (same regs) + * ──────────┼──────┼───────────────────────── + * DS3231 │ 0x68 │ DS1307, DS3232 + * PCF8563 │ 0x51 │ — (PCF85063A is 0x51 too but differs — override) + * RV3028 │ 0x52 │ — + * RX8130CE │ 0x32 │ — (RV8803 is 0x32 too but differs — override) + * + * status-reg 0xFF means the power-loss flag is in the seconds byte. A board + * with a same-address variant should /delete-node/ the conflicting one and + * add its own in the board overlay. + */ + +rtc_ds3231: rtc-ds3231@68 { + compatible = "zephcore,rtc-i2c"; + reg = <0x68>; + time-reg = <0x00>; + date-index = <4>; + status-reg = <0x0F>; /* control/status */ + status-mask = <0x80>; /* OSF — oscillator stopped */ +}; + +rtc_pcf8563: rtc-pcf8563@51 { + compatible = "zephcore,rtc-i2c"; + reg = <0x51>; + time-reg = <0x02>; + date-index = <3>; /* PCF8563 puts date before weekday */ + status-reg = <0xFF>; /* flag is in the seconds byte */ + status-mask = <0x80>; /* VL — clock integrity not guaranteed */ +}; + +rtc_rv3028: rtc-rv3028@52 { + compatible = "zephcore,rtc-i2c"; + reg = <0x52>; + time-reg = <0x00>; + date-index = <4>; + status-reg = <0x0e>; /* status */ + status-mask = <0x01>; /* PORF — power-on reset */ +}; + +rtc_rx8130ce: rtc-rx8130ce@32 { + compatible = "zephcore,rtc-i2c"; + reg = <0x32>; + time-reg = <0x10>; + date-index = <4>; + status-reg = <0x1d>; /* flag */ + status-mask = <0x02>; /* VLF — voltage low */ +}; diff --git a/zephcore/boards/common/sensors-i2c.dtsi b/zephcore/boards/common/sensors-i2c.dtsi index db83ea2..59f508f 100644 --- a/zephcore/boards/common/sensors-i2c.dtsi +++ b/zephcore/boards/common/sensors-i2c.dtsi @@ -103,3 +103,8 @@ ina219: ina219@40 { shunt-milliohm = <100>; lsb-microamp = <10>; }; + +/* Real-time clocks are NOT here — they live in the opt-in "rtc-i2c.dtsi" + * (included only by boards that physically have a battery-backed RTC), so + * their fixed addresses (0x68/0x51/0x52/0x32) can't collide with an IMU or + * other peripheral on boards that don't. */ diff --git a/zephcore/boards/nrf52840/lilygo_techo/lilygo_techo.dts b/zephcore/boards/nrf52840/lilygo_techo/lilygo_techo.dts index 1ace875..0d0757f 100644 --- a/zephcore/boards/nrf52840/lilygo_techo/lilygo_techo.dts +++ b/zephcore/boards/nrf52840/lilygo_techo/lilygo_techo.dts @@ -299,6 +299,9 @@ * devices that aren't populated. T-Echo factory sensor is BME280 at * the alt address 0x77, added separately below. */ #include "../../common/sensors-i2c.dtsi" + /* T-Echo has an onboard PCF8563 RTC — opt in to boot-time time restore + * (auto-detected at runtime among the supported chips). */ + #include "../../common/rtc-i2c.dtsi" }; /* T-Echo BME280 sits at I2C 0x77 (alt address), conflicting with the diff --git a/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts b/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts index 64a8321..ae99a36 100644 --- a/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts +++ b/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts @@ -287,6 +287,9 @@ /* All supported environment & power sensors — auto-detected at runtime */ #include "../../common/sensors-i2c.dtsi" + /* ThinkNode M1 has a battery-backed hardware RTC — opt in to boot-time + * time restore (auto-detected at runtime among the supported chips). */ + #include "../../common/rtc-i2c.dtsi" }; /* ---- SPI2 for LoRa SX1262 ---- */ diff --git a/zephcore/boards/nrf52840/thinknode_m3/thinknode_m3.dts b/zephcore/boards/nrf52840/thinknode_m3/thinknode_m3.dts index 37cc1cb..7d8557d 100644 --- a/zephcore/boards/nrf52840/thinknode_m3/thinknode_m3.dts +++ b/zephcore/boards/nrf52840/thinknode_m3/thinknode_m3.dts @@ -208,6 +208,9 @@ /* All supported environment & power sensors — auto-detected at runtime */ #include "../../common/sensors-i2c.dtsi" + /* ThinkNode M3 has an onboard PCF8563 RTC — opt in to boot-time time + * restore (auto-detected at runtime among the supported chips). */ + #include "../../common/rtc-i2c.dtsi" }; /* SPI1 for LR1110 */ diff --git a/zephcore/boards/nrf52840/thinknode_m6/thinknode_m6.dts b/zephcore/boards/nrf52840/thinknode_m6/thinknode_m6.dts index 05c3d56..86e9b27 100644 --- a/zephcore/boards/nrf52840/thinknode_m6/thinknode_m6.dts +++ b/zephcore/boards/nrf52840/thinknode_m6/thinknode_m6.dts @@ -241,6 +241,9 @@ /* All supported environment & power sensors — auto-detected at runtime */ #include "../../common/sensors-i2c.dtsi" + /* ThinkNode M6 has an onboard PCF8563 RTC — opt in to boot-time time + * restore (auto-detected at runtime among the supported chips). */ + #include "../../common/rtc-i2c.dtsi" }; /* SPI1 for SX1262 LoRa */ diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml new file mode 100644 index 0000000..f556603 --- /dev/null +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Data-only descriptor for an I2C RTC chip read by ZephyrRTCDiscover +# (adapters/clock/ZephyrRTCDiscover.c). This binds NO Zephyr RTC driver — +# the chip is accessed with raw I2C using the register layout described by +# the properties below. This keeps the boot-time RTC restore compact +# (~2-3 KB total, "Path B") instead of pulling in the full RTC subsystem. +# +# All supported chips use a 7-byte BCD time block (sec, min, hour, then +# weekday/date, month, year) starting at "time-reg". Only the day-of-month +# byte offset varies between parts, captured by "date-index". + +description: ZephCore data-only I2C RTC descriptor (raw-I2C, no Zephyr RTC driver) + +compatible: "zephcore,rtc-i2c" + +include: [i2c-device.yaml] + +properties: + time-reg: + type: int + required: true + description: Register address of the seconds byte (start of the 7-byte BCD block). + + date-index: + type: int + required: true + description: > + Byte offset of the day-of-month within the 7-byte block. 4 for chips + ordered sec/min/hour/weekday/date/month/year (DS3231, RV3028, RX8130CE); + 3 for PCF8563 which swaps to sec/min/hour/date/weekday/month/year. + + status-reg: + type: int + required: true + description: > + Register holding the "time invalid / power was lost" flag. Use 0xFF to + indicate the flag lives in the seconds byte itself (PCF8563 VL bit). + + status-mask: + type: int + required: true + description: Bit mask within status-reg that means the held time is unreliable. diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 6ecd9d5..624b3ca 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -6,6 +6,7 @@ #include "CommonCLI.h" #include "battery_curve.h" #include +#include #include #include #include @@ -336,6 +337,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch if (sender_timestamp > curr) { getRTCClock()->setCurrentTime(sender_timestamp + 1); time_sync_report(TIME_SYNC_CLI); + zephcore_rtc_save(sender_timestamp + 1); /* persist to hardware RTC */ uint32_t now = getRTCClock()->getCurrentTime(); time_t t = (time_t)now; struct tm *tm = gmtime(&t); @@ -356,6 +358,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch if (secs > curr) { getRTCClock()->setCurrentTime(secs); time_sync_report(TIME_SYNC_CLI); + zephcore_rtc_save(secs); /* persist to hardware RTC */ time_t t = (time_t)secs; struct tm *tm = gmtime(&t); snprintf(reply, CLI_REPLY_SIZE, "OK - clock set: %02d:%02d - %d/%d/%d UTC", diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index ffda59d..d677c54 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -21,6 +21,7 @@ LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); #include #include +#include #include #include #include @@ -754,6 +755,7 @@ static void gps_fix_callback(double lat, double lon, int64_t utc_time) LOG_INF("GPS fix: RTC sync time=%lld", utc_time); rtc_clock.setCurrentTime((uint32_t)utc_time); time_sync_report(TIME_SYNC_GPS); + zephcore_rtc_save((uint32_t)utc_time); /* persist to hardware RTC */ } #ifdef ZEPHCORE_LORA @@ -855,6 +857,16 @@ int main(void) /* Initialize sensor manager (GPS, environment sensors) */ sensor_manager_init(); + /* Restore wall-clock time from a battery-backed hardware RTC if one is + * present on I2C. Shown tagged "L" (local) until the next GPS/app/CLI + * sync; no-op on boards without an RTC. */ + { + uint32_t rtc_epoch; + if (zephcore_rtc_restore(&rtc_epoch)) { + rtc_clock.setCurrentTime(rtc_epoch); + } + } + /* Initialize UI subsystem (buttons, buzzer, display). * If display is present, ui_init() handles display init + auto-off. * If no display, fall back to raw OLED sleep for power saving. */ diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp index cf16f55..f58b4fd 100644 --- a/zephcore/src/main_repeater.cpp +++ b/zephcore/src/main_repeater.cpp @@ -51,6 +51,7 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) #include #include #include +#include #include /* UI subsystem (display, buttons, buzzer) */ @@ -271,6 +272,7 @@ static void gps_fix_callback(double lat, double lon, int64_t utc_time) if (utc_time > 0) { LOG_INF("GPS fix: RTC sync time=%lld", utc_time); rtc_clock.setCurrentTime((uint32_t)utc_time); + zephcore_rtc_save((uint32_t)utc_time); /* persist to hardware RTC */ } int lat_deg = (int)lat; @@ -431,6 +433,15 @@ int main(void) /* Initialize sensor manager */ sensor_manager_init(); + /* Restore wall-clock time from a battery-backed hardware RTC if present + * (shown tagged "L" until the next GPS/CLI sync; no-op if no RTC). */ + { + uint32_t rtc_epoch; + if (zephcore_rtc_restore(&rtc_epoch)) { + rtc_clock.setCurrentTime(rtc_epoch); + } + } + /* Set GPS to repeater mode: power off now, wake every 48h for time sync only. * This prevents GPS from draining power on boards that have it (e.g., Wio Tracker). */ if (gps_is_available()) { diff --git a/zephcore/src/main_room_server.cpp b/zephcore/src/main_room_server.cpp index b4d90c6..93b90c7 100644 --- a/zephcore/src/main_room_server.cpp +++ b/zephcore/src/main_room_server.cpp @@ -51,6 +51,7 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) #include #include #include +#include #include /* UI subsystem (display, buttons, buzzer) */ @@ -284,6 +285,7 @@ static void gps_fix_callback(double lat, double lon, int64_t utc_time) if (utc_time > 0) { LOG_INF("GPS fix: RTC sync time=%lld", utc_time); rtc_clock.setCurrentTime((uint32_t)utc_time); + zephcore_rtc_save((uint32_t)utc_time); /* persist to hardware RTC */ } int lat_deg = (int)lat; @@ -449,6 +451,15 @@ int main(void) /* Initialize sensor manager */ sensor_manager_init(); + /* Restore wall-clock time from a battery-backed hardware RTC if present + * (shown tagged "L" until the next GPS/CLI sync; no-op if no RTC). */ + { + uint32_t rtc_epoch; + if (zephcore_rtc_restore(&rtc_epoch)) { + rtc_clock.setCurrentTime(rtc_epoch); + } + } + /* Set GPS to repeater mode: power off now, wake every 48h for time sync only. * This prevents GPS from draining power on boards that have it (e.g., Wio Tracker). */ if (gps_is_available()) {