From 72bfc7b63e6a1ad8578ad2c64f6b847c6c0ac7c5 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 13:26:14 -0700 Subject: [PATCH 01/26] rak4631: declare the RAK12002 RTC for the boot-time probe The RAK4631 overlay declares no zephcore,rtc-i2c candidate, so a fitted RAK12002 (RV3028 at 0x52) is never probed or written. The node keeps the time across a warm reboot only in noinit RAM, so any reset that loses RAM starts it on the soft clock until the next GPS, app or CLI time sync. Declare the RV3028 alone, with the descriptor rtc-i2c.dtsi uses. Not the whole of that file: its DS3231 candidate at 0x68 shares the address of WisBlock IMUs. 0x52 is clear of every address sensors-i2c.dtsi declares. A board without the module fails the probe and is skipped. Co-Authored-By: Claude Opus 5.5 --- zephcore/boards/nrf52840/rak4631/board.overlay | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index a0ada57c..f9c41c4f 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -215,6 +215,20 @@ }; #include "../../common/sensors-i2c.dtsi" + + /* + * A WisBlock slot can carry a RAK12002 (RV3028 at 0x52). Only that + * candidate, not all of rtc-i2c.dtsi: its DS3231 at 0x68 shares the + * address of WisBlock IMUs. + */ + 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 */ + }; }; /* RAK4631 has BME680, not BME280 — override the common DTSI default */ From 36d5ca94c20775b9acf90f04d155bef543bd26be Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 13:26:14 -0700 Subject: [PATCH 02/26] rtc: never adopt a candidate whose time block reads all 0xFF A real RTC resets its time registers to defined values. An erased EEPROM at a candidate's address reads 0xFF everywhere, its status register too, and 0xFF has the power-loss bit set in every descriptor's mask, so the probe adopted it as a lost-power RTC and every sync wrote the time into it. 24-series EEPROMs answer within 0x50-0x57, which includes the RV3028's 0x52 that the RAK4631 now declares, so a WisBlock EEPROM module fitted without a RAK12002 could be taken for one. Skip a candidate whose seven time bytes are all 0xFF. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index db373056..af8f01da 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -124,6 +124,18 @@ static bool rtc_probe(uint32_t *epoch_out) continue; /* no ACK => chip absent */ } + /* An RTC resets its time to defined values, never all 0xFF. An erased + * EEPROM sharing the address reads 0xFF everywhere, its "status" too, + * which would otherwise pass as a set power-loss flag. */ + bool all_ff = true; + + for (size_t k = 0; k < sizeof(blk); k++) { + all_ff = all_ff && (blk[k] == 0xFF); + } + if (all_ff) { + continue; + } + /* Mask off flag/century bits. We trust this is a real RTC (vs. an * unrelated chip sharing the address) if EITHER the block is valid * BCD, OR the chip's power-loss flag is set — the latter is itself From ab6cb7c8518ebc4e0399a258b76d7625dfc25201 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 13:26:14 -0700 Subject: [PATCH 03/26] rtc: enable the RAK12002's backup switchover and keep it set The RV3028 ships with backup switchover disabled (EEPROM 37h, BSM = 00), so a RAK12002 lost the time on every power cut although its supercapacitor could carry it: after removing all power the chip came back with PORF set. An optional descriptor property, rv3028-eeprom-config, lists (register, mask, value) triplets for the chip's EEPROM-backed configuration. When the chip is adopted, any register whose masked bits differ is written in RAM and committed with the EEPROM Update command, following the RV-3028-C7 Application Manual's own procedure (rev 1.4, 4.6.9: EERD = 1, edit 35h-37h, EECMD 00h then 11h, EERD = 0), as Zephyr's drivers/mfd/mfd_rv3028.c does. Committing to EEPROM matters: with EERD = 0 the chip refreshes its configuration RAM from EEPROM at power-on and every 24 hours (Control 1 register, EERD; manual p. 39), so a RAM-only setting reverts within a day. An unchanged chip costs no EEPROM write. The RAK4631 overlay sets CLKOUT off (35h CLKOE) and, in 37h, direct switching mode with the trickle charger on through 3 kOhm (TCE, BSM, TCR), leaving the factory-calibrated EEOffset bit and FEDE alone. RAK documents the RAK12002's backup as a supercapacitor, so charging it is right; the shared rtc-i2c.dtsi sets nothing, since other boards' backup supplies are not known. Fixes fork issue #33. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 88 +++++++++++++++++++ .../boards/nrf52840/rak4631/board.overlay | 3 + .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 11 +++ 3 files changed, 102 insertions(+) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index af8f01da..25235ead 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -33,9 +33,20 @@ struct rtc_desc { 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 uint8_t *cfg; /* rv3028-eeprom-config triplets, or NULL */ + uint8_t cfg_len; const char *name; }; +#define RTC_CFG_NAME(node) _CONCAT(rtc_cfg_, DT_DEP_ORD(node)) + +#define RTC_CFG_ARRAY(node) \ + IF_ENABLED(DT_NODE_HAS_PROP(node, rv3028_eeprom_config), \ + (static const uint8_t RTC_CFG_NAME(node)[] = \ + DT_PROP(node, rv3028_eeprom_config);)) + +DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_CFG_ARRAY) + #define RTC_DESC_ENTRY(node) \ { \ .bus = DEVICE_DT_GET(DT_BUS(node)), \ @@ -44,6 +55,10 @@ struct rtc_desc { .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), \ + .cfg = COND_CODE_1( \ + DT_NODE_HAS_PROP(node, rv3028_eeprom_config), \ + (RTC_CFG_NAME(node)), (NULL)), \ + .cfg_len = DT_PROP_LEN_OR(node, rv3028_eeprom_config, 0), \ .name = DT_NODE_FULL_NAME(node), \ }, @@ -56,6 +71,15 @@ static const struct rtc_desc *s_active; static bool s_probed; #define BCD2BIN(x) ((((x) >> 4) & 0x0F) * 10 + ((x) & 0x0F)) + +/* RV3028 registers for committing configuration to its EEPROM, as Zephyr's + * drivers/mfd/mfd_rv3028.c does. */ +#define RV3028_REG_STATUS 0x0E +#define RV3028_STATUS_EEBUSY BIT(7) +#define RV3028_REG_CONTROL1 0x0F +#define RV3028_CONTROL1_EERD BIT(3) +#define RV3028_REG_EE_COMMAND 0x27 +#define RV3028_EE_CMD_UPDATE 0x11 /* all configuration RAM -> EEPROM */ #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 @@ -109,6 +133,69 @@ static bool rtc_time_unreliable(const struct rtc_desc *d, const uint8_t blk[7]) return (st & d->status_mask) != 0; } +static bool rv3028_eeprom_idle(const struct rtc_desc *d) +{ + for (int ms = 0; ms < 100; ms += 10) { + uint8_t st; + + if (i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) != 0) { + return false; + } + if (!(st & RV3028_STATUS_EEBUSY)) { + return true; + } + k_msleep(10); + } + return false; +} + +/* Apply the descriptor's rv3028-eeprom-config to the adopted chip. Only a + * differing register is written, then RAM is committed to EEPROM with + * automatic refresh held off, so an unchanged chip costs no EEPROM write. */ +static void rv3028_apply_eeprom_config(const struct rtc_desc *d) +{ + bool changed = false, failed = false; + + if (d->cfg == NULL) { + return; + } + if (i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, + RV3028_CONTROL1_EERD, RV3028_CONTROL1_EERD) != 0 || + !rv3028_eeprom_idle(d)) { + LOG_WRN("%s: config not applied", d->name); + return; + } + for (size_t i = 0; i + 2 < d->cfg_len; i += 3) { + uint8_t old, reg = d->cfg[i], mask = d->cfg[i + 1]; + uint8_t val = d->cfg[i + 2]; + + if (i2c_reg_read_byte(d->bus, d->addr, reg, &old) != 0) { + failed = true; + break; + } + uint8_t new = (old & ~mask) | (val & mask); + + if (new != old) { + if (i2c_reg_write_byte(d->bus, d->addr, reg, new) != 0) { + failed = true; + break; + } + changed = true; + } + } + if (failed) { + LOG_WRN("%s: config not applied", d->name); + } else if (changed && + i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, 0x00) == 0 && + i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, + RV3028_EE_CMD_UPDATE) == 0 && + rv3028_eeprom_idle(d)) { + LOG_INF("%s: config committed to EEPROM", d->name); + } + (void)i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, + RV3028_CONTROL1_EERD, 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) @@ -159,6 +246,7 @@ static bool rtc_probe(uint32_t *epoch_out) if (s_active == NULL) { s_active = d; /* RTC => our write-back target */ + rv3028_apply_eeprom_config(d); } if (unreliable) { diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index f9c41c4f..865e8f80 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -228,6 +228,9 @@ date-index = <4>; status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ + /* The RAK12002's backup is a supercapacitor: CLKOUT off (0x35 + * CLKOE), direct switchover, trickle charge through 3 kOhm (0x37). */ + rv3028-eeprom-config = [35 80 00 37 2f 24]; }; }; diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 92f2e926..598f5958 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -41,3 +41,14 @@ properties: type: int required: true description: Bit mask within status-reg that means the held time is unreliable. + + rv3028-eeprom-config: + type: uint8-array + description: > + RV3028 only. (register, mask, value) triplets for the chip's + EEPROM-backed configuration registers, such as 0x35 (CLKOUT) and 0x37 + (backup switchover and trickle charger). When the chip is adopted, any + register whose masked bits differ is updated and committed to the + chip's EEPROM, so the chip's own refresh from EEPROM keeps it. Set it + only on a board whose backup supply is known: trickle charging suits + a supercapacitor or rechargeable cell, never a primary cell. From 778a9e3f30c286b45c1d831106c6292150611394 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 13:26:29 -0700 Subject: [PATCH 04/26] rtc: say what the all-0xFF guard costs a real RTC The comment said an RTC never reads all 0xFF. The RV3028 cannot: its time registers reset to 0 (RV-3028-C7 Application Manual, Seconds register). A chip that leaves its time undefined at first power-on could, and is then skipped for that boot only: no write-back until a later boot finds it with defined time. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 25235ead..5cb06665 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -211,9 +211,10 @@ static bool rtc_probe(uint32_t *epoch_out) continue; /* no ACK => chip absent */ } - /* An RTC resets its time to defined values, never all 0xFF. An erased - * EEPROM sharing the address reads 0xFF everywhere, its "status" too, - * which would otherwise pass as a set power-loss flag. */ + /* An erased EEPROM sharing the address reads 0xFF everywhere, its + * "status" too, which would otherwise pass as a set power-loss flag. + * A real RTC reading all 0xFF (some leave their time undefined at + * power-on) is skipped too, losing write-back for that boot only. */ bool all_ff = true; for (size_t k = 0; k < sizeof(blk); k++) { From cd9a0bb37891820c4bc61d581777a7ca595b78bf Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 15:15:41 -0700 Subject: [PATCH 05/26] rtc: compare against EEPROM, verify, and check the chip before RV3028 writes Review of the switchover change found the EEPROM step weaker than Zephyr's and the manual's: - The comparison was against the RAM mirror, which may hold a value no EEPROM ever got (a failed commit, or firmware that writes RAM only): the node then never commits, and the next midnight refresh reverts the setting. RAM is now refreshed from EEPROM first (EECMD 12h), as Zephyr's driver does, so the comparison is against what is stored. - After an EEPROM command it now waits 10 ms before polling EEbusy, as the manual requires (4.6.7), and every command's failure is reported. - The written registers are read back, so a password-locked chip (EEPWE set) reports "not applied" instead of "committed". - EERD was left set when the first EEbusy wait failed; it is now always cleared once it has been set. - Nothing RV3028-specific is written unless the time block's always-zero bits read 0 (Seconds b7, Minutes b7, Hours b7:6, Weekday b7:3, Date b7:6, Month b7:5; manual 3.2). Another part answering at the address, a 24-series EEPROM for one, is then left alone. - A BUILD_ASSERT requires the property to be whole triplets. Corrections to the previous commit's message: the 24-hour automatic refresh is in Control 1, EERD (manual p. 23, and 4.6.2 p. 54); the power-on refresh happens whatever EERD is (4.6.1). Direct switching is what the manual's rechargeable-backup application circuit specifies for a capacitor (7.3, p. 105: "DSM Backup Switchover Mode for capacitors (or LSM for rechargeable battery)"), although 4.2.2 cautions against DSM when VDD and VBACKUP are close. CLKOUT off saves VDD current; CLKOUT is already off on backup. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 106 ++++++++++++------ .../boards/nrf52840/rak4631/board.overlay | 6 +- 2 files changed, 75 insertions(+), 37 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 5cb06665..9b9f94a5 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -43,7 +43,9 @@ struct rtc_desc { #define RTC_CFG_ARRAY(node) \ IF_ENABLED(DT_NODE_HAS_PROP(node, rv3028_eeprom_config), \ (static const uint8_t RTC_CFG_NAME(node)[] = \ - DT_PROP(node, rv3028_eeprom_config);)) + DT_PROP(node, rv3028_eeprom_config); \ + BUILD_ASSERT(sizeof(RTC_CFG_NAME(node)) % 3 == 0, \ + "rv3028-eeprom-config is (register, mask, value) triplets");)) DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_CFG_ARRAY) @@ -80,6 +82,7 @@ static bool s_probed; #define RV3028_CONTROL1_EERD BIT(3) #define RV3028_REG_EE_COMMAND 0x27 #define RV3028_EE_CMD_UPDATE 0x11 /* all configuration RAM -> EEPROM */ +#define RV3028_EE_CMD_REFRESH 0x12 /* all configuration EEPROM -> RAM */ #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 @@ -133,15 +136,14 @@ static bool rtc_time_unreliable(const struct rtc_desc *d, const uint8_t blk[7]) return (st & d->status_mask) != 0; } +/* Wait for EEbusy to clear: ~66 ms after power-on, ~63 ms for an Update. */ static bool rv3028_eeprom_idle(const struct rtc_desc *d) { - for (int ms = 0; ms < 100; ms += 10) { + for (int ms = 0; ms <= 100; ms += 10) { uint8_t st; - if (i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) != 0) { - return false; - } - if (!(st & RV3028_STATUS_EEBUSY)) { + if (i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) == 0 && + !(st & RV3028_STATUS_EEBUSY)) { return true; } k_msleep(10); @@ -149,51 +151,85 @@ static bool rv3028_eeprom_idle(const struct rtc_desc *d) return false; } -/* Apply the descriptor's rv3028-eeprom-config to the adopted chip. Only a - * differing register is written, then RAM is committed to EEPROM with - * automatic refresh held off, so an unchanged chip costs no EEPROM write. */ -static void rv3028_apply_eeprom_config(const struct rtc_desc *d) +static bool rv3028_eeprom_cmd(const struct rtc_desc *d, uint8_t cmd) +{ + if (i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, 0x00) != 0 || + i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, cmd) != 0) { + return false; + } + k_msleep(10); /* the manual: wait 10 ms before checking EEbusy */ + return rv3028_eeprom_idle(d); +} + +/* Bits the RV3028 always reads as 0 in its time block (24-hour mode). A + * chip that sets any is not an RV3028, so nothing RV3028-specific is + * written to it. */ +static bool rv3028_time_bits_ok(const uint8_t blk[7]) +{ + static const uint8_t zero[7] = { 0x80, 0x80, 0xC0, 0xF8, 0xC0, 0xE0, 0x00 }; + + for (size_t k = 0; k < 7; k++) { + if (blk[k] & zero[k]) { + return false; + } + } + return true; +} + +/* Apply the descriptor's rv3028-eeprom-config to the adopted chip, as Zephyr's + * drivers/mfd/mfd_rv3028.c does: refresh RAM from EEPROM so the comparison + * is against what is stored, write only what differs, commit it with the + * Update command, read it back. Automatic refresh is held off throughout + * and always re-enabled. */ +static void rv3028_apply_eeprom_config(const struct rtc_desc *d, const uint8_t blk[7]) { - bool changed = false, failed = false; + bool changed = false, ok = true; if (d->cfg == NULL) { return; } - if (i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, - RV3028_CONTROL1_EERD, RV3028_CONTROL1_EERD) != 0 || - !rv3028_eeprom_idle(d)) { + if (!rv3028_time_bits_ok(blk)) { + LOG_WRN("%s: not an RV3028, config skipped", d->name); + return; + } + if (!rv3028_eeprom_idle(d) || + i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, + RV3028_CONTROL1_EERD, RV3028_CONTROL1_EERD) != 0) { LOG_WRN("%s: config not applied", d->name); return; } - for (size_t i = 0; i + 2 < d->cfg_len; i += 3) { + ok = rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH); + + for (size_t i = 0; ok && i + 2 < d->cfg_len; i += 3) { uint8_t old, reg = d->cfg[i], mask = d->cfg[i + 1]; uint8_t val = d->cfg[i + 2]; - if (i2c_reg_read_byte(d->bus, d->addr, reg, &old) != 0) { - failed = true; - break; - } - uint8_t new = (old & ~mask) | (val & mask); - - if (new != old) { - if (i2c_reg_write_byte(d->bus, d->addr, reg, new) != 0) { - failed = true; - break; - } + ok = i2c_reg_read_byte(d->bus, d->addr, reg, &old) == 0; + if (ok && ((old ^ val) & mask) != 0) { + ok = i2c_reg_write_byte(d->bus, d->addr, reg, + (old & ~mask) | (val & mask)) == 0; changed = true; } } - if (failed) { + if (ok && changed) { + ok = rv3028_eeprom_cmd(d, RV3028_EE_CMD_UPDATE); + } + for (size_t i = 0; ok && changed && i + 2 < d->cfg_len; i += 3) { + uint8_t now; + + ok = i2c_reg_read_byte(d->bus, d->addr, d->cfg[i], &now) == 0 && + ((now ^ d->cfg[i + 2]) & d->cfg[i + 1]) == 0; + } + + if (i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, + RV3028_CONTROL1_EERD, 0) != 0) { + ok = false; + } + if (!ok) { LOG_WRN("%s: config not applied", d->name); - } else if (changed && - i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, 0x00) == 0 && - i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, - RV3028_EE_CMD_UPDATE) == 0 && - rv3028_eeprom_idle(d)) { + } else if (changed) { LOG_INF("%s: config committed to EEPROM", d->name); } - (void)i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, - RV3028_CONTROL1_EERD, 0); } /* Probe all chips once; cache the first present one in s_active. If a present @@ -247,7 +283,7 @@ static bool rtc_probe(uint32_t *epoch_out) if (s_active == NULL) { s_active = d; /* RTC => our write-back target */ - rv3028_apply_eeprom_config(d); + rv3028_apply_eeprom_config(d, blk); } if (unreliable) { diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index 865e8f80..52360331 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -228,8 +228,10 @@ date-index = <4>; status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ - /* The RAK12002's backup is a supercapacitor: CLKOUT off (0x35 - * CLKOE), direct switchover, trickle charge through 3 kOhm (0x37). */ + /* The RAK12002's backup is a supercapacitor. As the RV-3028-C7 + * manual's rechargeable-backup circuit (7.3): CLKOUT off (0x35 + * CLKOE, unused), direct switchover with trickle charge through + * 3 kOhm (0x37 TCE, BSM, TCR). */ rv3028-eeprom-config = [35 80 00 37 2f 24]; }; }; From 6724da5be9c3d86e9c0bb237f6d3dc702c95af30 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 16:21:12 -0700 Subject: [PATCH 06/26] rtc: do not adopt a non-RV3028 at an RV3028 descriptor; config 35h-37h only The always-zero-bit check skipped only the configuration step. A chip that failed it was still adopted, and every time sync wrote seven bytes and the status byte into it: a programmed 24-series EEPROM answering at the RAK4631's 0x52 would lose data. At a descriptor that carries rv3028-eeprom-config, such a chip is now not adopted at all. rv3028-eeprom-config now acts only on 35h-37h, the EEPROM-backed configuration registers it is for; any other register is logged and ignored, so a devicetree typo cannot, say, enable password protection or rewrite EEPROM on every boot. The wait before polling EEbusy is now 1 ms after a Refresh and 10 ms after an Update, as the manual gives for reads and writes (4.6.7). Corrections to earlier messages and comments: the 3 kOhm trickle resistor is the delivery default (37h TCR = 00), not a choice section 7.3 makes; the read-back is ZephCore's, not something Zephyr's driver does; and a chip with EEPWE set is locked only while its password register does not match (4.18.1), so "password-locked" means that. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 47 ++++++++++++------- .../boards/nrf52840/rak4631/board.overlay | 6 +-- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 6 ++- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 9b9f94a5..eab06a90 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -83,6 +83,8 @@ static bool s_probed; #define RV3028_REG_EE_COMMAND 0x27 #define RV3028_EE_CMD_UPDATE 0x11 /* all configuration RAM -> EEPROM */ #define RV3028_EE_CMD_REFRESH 0x12 /* all configuration EEPROM -> RAM */ +#define RV3028_REG_CFG_FIRST 0x35 /* EEPROM Clkout */ +#define RV3028_REG_CFG_LAST 0x37 /* EEPROM Backup */ #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 @@ -151,19 +153,20 @@ static bool rv3028_eeprom_idle(const struct rtc_desc *d) return false; } -static bool rv3028_eeprom_cmd(const struct rtc_desc *d, uint8_t cmd) +/* The manual (4.6.7): wait 10 ms after an EEPROM write or Update, 1 ms after + * a read or Refresh, before checking EEbusy. */ +static bool rv3028_eeprom_cmd(const struct rtc_desc *d, uint8_t cmd, int32_t wait_ms) { if (i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, 0x00) != 0 || i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, cmd) != 0) { return false; } - k_msleep(10); /* the manual: wait 10 ms before checking EEbusy */ + k_msleep(wait_ms); return rv3028_eeprom_idle(d); } -/* Bits the RV3028 always reads as 0 in its time block (24-hour mode). A - * chip that sets any is not an RV3028, so nothing RV3028-specific is - * written to it. */ +/* Bits the RV3028 always reads as 0 in its time block (manual 3.2). A chip + * at an RV3028 descriptor that sets any is not one, and is not adopted. */ static bool rv3028_time_bits_ok(const uint8_t blk[7]) { static const uint8_t zero[7] = { 0x80, 0x80, 0xC0, 0xF8, 0xC0, 0xE0, 0x00 }; @@ -176,34 +179,34 @@ static bool rv3028_time_bits_ok(const uint8_t blk[7]) return true; } -/* Apply the descriptor's rv3028-eeprom-config to the adopted chip, as Zephyr's - * drivers/mfd/mfd_rv3028.c does: refresh RAM from EEPROM so the comparison - * is against what is stored, write only what differs, commit it with the - * Update command, read it back. Automatic refresh is held off throughout - * and always re-enabled. */ -static void rv3028_apply_eeprom_config(const struct rtc_desc *d, const uint8_t blk[7]) +/* Apply the descriptor's rv3028-eeprom-config to the adopted chip, by the + * manual's procedure (4.6.3, 4.6.9): refresh RAM from EEPROM so the + * comparison is against what is stored, write only what differs, commit it + * with the Update command, then read it back. Automatic refresh is held off + * throughout and always re-enabled. Only 35h-37h are accepted. */ +static void rv3028_apply_eeprom_config(const struct rtc_desc *d) { bool changed = false, ok = true; if (d->cfg == NULL) { return; } - if (!rv3028_time_bits_ok(blk)) { - LOG_WRN("%s: not an RV3028, config skipped", d->name); - return; - } if (!rv3028_eeprom_idle(d) || i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, RV3028_CONTROL1_EERD, RV3028_CONTROL1_EERD) != 0) { LOG_WRN("%s: config not applied", d->name); return; } - ok = rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH); + ok = rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH, 1); for (size_t i = 0; ok && i + 2 < d->cfg_len; i += 3) { uint8_t old, reg = d->cfg[i], mask = d->cfg[i + 1]; uint8_t val = d->cfg[i + 2]; + if (reg < RV3028_REG_CFG_FIRST || reg > RV3028_REG_CFG_LAST) { + LOG_WRN("%s: config register 0x%02x ignored", d->name, reg); + continue; + } ok = i2c_reg_read_byte(d->bus, d->addr, reg, &old) == 0; if (ok && ((old ^ val) & mask) != 0) { ok = i2c_reg_write_byte(d->bus, d->addr, reg, @@ -212,11 +215,14 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d, const uint8_t b } } if (ok && changed) { - ok = rv3028_eeprom_cmd(d, RV3028_EE_CMD_UPDATE); + ok = rv3028_eeprom_cmd(d, RV3028_EE_CMD_UPDATE, 10); } for (size_t i = 0; ok && changed && i + 2 < d->cfg_len; i += 3) { uint8_t now; + if (d->cfg[i] < RV3028_REG_CFG_FIRST || d->cfg[i] > RV3028_REG_CFG_LAST) { + continue; + } ok = i2c_reg_read_byte(d->bus, d->addr, d->cfg[i], &now) == 0 && ((now ^ d->cfg[i + 2]) & d->cfg[i + 1]) == 0; } @@ -281,9 +287,14 @@ static bool rtc_probe(uint32_t *epoch_out) continue; /* neither valid time nor a lost-power RTC => skip */ } + if (d->cfg != NULL && !rv3028_time_bits_ok(blk)) { + LOG_WRN("%s: not an RV3028, skipped", d->name); + continue; + } + if (s_active == NULL) { s_active = d; /* RTC => our write-back target */ - rv3028_apply_eeprom_config(d, blk); + rv3028_apply_eeprom_config(d); } if (unreliable) { diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index 52360331..8bf0b70e 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -229,9 +229,9 @@ status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ /* The RAK12002's backup is a supercapacitor. As the RV-3028-C7 - * manual's rechargeable-backup circuit (7.3): CLKOUT off (0x35 - * CLKOE, unused), direct switchover with trickle charge through - * 3 kOhm (0x37 TCE, BSM, TCR). */ + * manual's rechargeable-backup circuit (7.3): direct switchover + * with trickle charging (0x37 TCE, BSM), TCR at its 3 kOhm delivery + * default; CLKOUT off (0x35 CLKOE), as it is unused. */ rv3028-eeprom-config = [35 80 00 37 2f 24]; }; }; diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 598f5958..47ff9d85 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -46,8 +46,10 @@ properties: type: uint8-array description: > RV3028 only. (register, mask, value) triplets for the chip's - EEPROM-backed configuration registers, such as 0x35 (CLKOUT) and 0x37 - (backup switchover and trickle charger). When the chip is adopted, any + EEPROM-backed configuration registers 0x35 (CLKOUT) to 0x37 (backup + switchover and trickle charger); other registers are ignored. A chip + at the address whose always-zero time bits read 1 is not adopted. + When the chip is adopted, any register whose masked bits differ is updated and committed to the chip's EEPROM, so the chip's own refresh from EEPROM keeps it. Set it only on a board whose backup supply is known: trickle charging suits From d59f98d9fbef2bfbe6939b97609452610b8c7d4d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 16:35:15 -0700 Subject: [PATCH 07/26] rtc: re-probe a skipped all-0xFF candidate; tighten config triplets From Copilot's review of #35 and the last local pass: - A real RTC whose time block reads all 0xFF (some leave it undefined at power-on) was skipped and, with the probe run once at boot, never adopted for that boot: no sync was persisted. A save now probes again when a candidate was skipped that way and nothing was adopted; a real RTC's counter has moved off all 0xFF by then, an erased EEPROM has not. - A rv3028-eeprom-config triplet whose mask covers an unimplemented bit (35h bits 5:4, manual 3.15.4) or that repeats a register made every boot see a difference and run an EEPROM Update. Such triplets, like those outside 35h-37h, are now ignored with a warning, and the binding says so. - The config step's comment now cites each step's section: refresh is 4.6.4 (and Zephyr's init), the read-back is ours. - ZEPHCORE_RTC_AUTODISCOVER's help no longer says boards opt in only by including rtc-i2c.dtsi, and mentions the EEPROM configuration write. Co-Authored-By: Claude Opus 5.5 --- zephcore/Kconfig | 14 ++--- zephcore/adapters/clock/ZephyrRTCDiscover.c | 52 +++++++++++++++---- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 4 +- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 444b11de..4119ff24 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -899,12 +899,14 @@ config ZEPHCORE_RTC_AUTODISCOVER 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. + backed RTC opt in by including boards/common/rtc-i2c.dtsi or declaring + their own node; 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. A node with + rv3028-eeprom-config also has that configuration committed to the + chip's EEPROM when it differs. Compact raw-I2C reader (no Zephyr RTC + subsystem). Safe to leave on: a board that declares no zephcore,rtc-i2c + node compiles to nothing. menu "GPS Configuration" diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index eab06a90..b9203db0 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -71,6 +71,8 @@ static const struct rtc_desc rtc_descs[] = { /* Chip we'll read/write going forward (first one found present). */ static const struct rtc_desc *s_active; static bool s_probed; +/* A candidate read all 0xFF and was skipped; see zephcore_rtc_save(). */ +static bool s_skipped_ff; #define BCD2BIN(x) ((((x) >> 4) & 0x0F) * 10 + ((x) & 0x0F)) @@ -165,6 +167,26 @@ static bool rv3028_eeprom_cmd(const struct rtc_desc *d, uint8_t cmd, int32_t wai return rv3028_eeprom_idle(d); } +/* A config triplet is used only if its register is 35h-37h, its mask covers + * implemented bits only (35h bits 5:4 are not; manual 3.15.4), and no + * earlier triplet names the same register. */ +static bool rv3028_cfg_triplet_ok(const struct rtc_desc *d, size_t i) +{ + static const uint8_t impl[] = { 0xCF, 0xFF, 0xFF }; /* 35h, 36h, 37h */ + uint8_t reg = d->cfg[i]; + + if (reg < RV3028_REG_CFG_FIRST || reg > RV3028_REG_CFG_LAST || + (d->cfg[i + 1] & ~impl[reg - RV3028_REG_CFG_FIRST]) != 0) { + return false; + } + for (size_t k = 0; k < i; k += 3) { + if (d->cfg[k] == reg) { + return false; + } + } + return true; +} + /* Bits the RV3028 always reads as 0 in its time block (manual 3.2). A chip * at an RV3028 descriptor that sets any is not one, and is not adopted. */ static bool rv3028_time_bits_ok(const uint8_t blk[7]) @@ -179,11 +201,13 @@ static bool rv3028_time_bits_ok(const uint8_t blk[7]) return true; } -/* Apply the descriptor's rv3028-eeprom-config to the adopted chip, by the - * manual's procedure (4.6.3, 4.6.9): refresh RAM from EEPROM so the - * comparison is against what is stored, write only what differs, commit it - * with the Update command, then read it back. Automatic refresh is held off - * throughout and always re-enabled. Only 35h-37h are accepted. */ +/* Apply the descriptor's rv3028-eeprom-config to the adopted chip: hold off + * automatic refresh (manual 4.6.3, 4.6.9), refresh RAM from EEPROM so the + * comparison is against what is stored (4.6.4, as Zephyr's driver does), + * write only what differs, commit it with the Update command (4.6.3), and + * read it back (ours). Refresh is always re-enabled. A triplet outside + * 35h-37h, masking an unimplemented bit, or repeating a register is + * ignored, so a devicetree mistake cannot force an Update on every boot. */ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) { bool changed = false, ok = true; @@ -203,8 +227,8 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) uint8_t old, reg = d->cfg[i], mask = d->cfg[i + 1]; uint8_t val = d->cfg[i + 2]; - if (reg < RV3028_REG_CFG_FIRST || reg > RV3028_REG_CFG_LAST) { - LOG_WRN("%s: config register 0x%02x ignored", d->name, reg); + if (!rv3028_cfg_triplet_ok(d, i)) { + LOG_WRN("%s: config triplet for 0x%02x ignored", d->name, reg); continue; } ok = i2c_reg_read_byte(d->bus, d->addr, reg, &old) == 0; @@ -220,7 +244,7 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) for (size_t i = 0; ok && changed && i + 2 < d->cfg_len; i += 3) { uint8_t now; - if (d->cfg[i] < RV3028_REG_CFG_FIRST || d->cfg[i] > RV3028_REG_CFG_LAST) { + if (!rv3028_cfg_triplet_ok(d, i)) { continue; } ok = i2c_reg_read_byte(d->bus, d->addr, d->cfg[i], &now) == 0 && @@ -242,6 +266,8 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) * chip holds a sane time, return it via epoch_out. */ static bool rtc_probe(uint32_t *epoch_out) { + s_skipped_ff = false; + for (size_t i = 0; i < ARRAY_SIZE(rtc_descs); i++) { const struct rtc_desc *d = &rtc_descs[i]; uint8_t blk[7]; @@ -255,14 +281,15 @@ static bool rtc_probe(uint32_t *epoch_out) /* An erased EEPROM sharing the address reads 0xFF everywhere, its * "status" too, which would otherwise pass as a set power-loss flag. - * A real RTC reading all 0xFF (some leave their time undefined at - * power-on) is skipped too, losing write-back for that boot only. */ + * A real RTC can read all 0xFF too (some leave their time undefined + * at power-on), so a later save probes again. */ bool all_ff = true; for (size_t k = 0; k < sizeof(blk); k++) { all_ff = all_ff && (blk[k] == 0xFF); } if (all_ff) { + s_skipped_ff = true; continue; } @@ -341,6 +368,11 @@ void zephcore_rtc_save(uint32_t epoch) (void)rtc_probe(NULL); s_probed = true; } + if (s_active == NULL && s_skipped_ff) { + /* An RTC's counter has moved its time off all 0xFF by now; an + * erased EEPROM has not, and is skipped again. */ + (void)rtc_probe(NULL); + } if (s_active == NULL) { return; } diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 47ff9d85..084c582e 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -47,7 +47,9 @@ properties: description: > RV3028 only. (register, mask, value) triplets for the chip's EEPROM-backed configuration registers 0x35 (CLKOUT) to 0x37 (backup - switchover and trickle charger); other registers are ignored. A chip + switchover and trickle charger), one triplet per register, masking + implemented bits only (35h bits 5:4 are not); any other triplet is + ignored. A chip at the address whose always-zero time bits read 1 is not adopted. When the chip is adopted, any register whose masked bits differ is updated and committed to the From 9d52ff4ba0888cfec536dcbf27115102e7b56a8b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 16:51:13 -0700 Subject: [PATCH 08/26] rtc: verify the commit against EEPROM, re-probe once, state the DSM caution From the local pass over the review-fix push: - The read-back after an Update read the RAM mirror, which keeps what was written whether or not the EEPROM write completed, so "config committed to EEPROM" could be logged for a commit that did not land. A Refresh now follows the Update, so the read-back sees the EEPROM. Checked on a RAK4631 + RAK12002 by flashing a build with TCR = 01 and then the real one: each first boot logged "config committed to EEPROM" and restored the time. - The all-0xFF re-probe ran on every save while nothing was adopted, giving a non-RTC on a board with several descriptors a fresh chance at adoption each time. It now runs once per boot, and its comment no longer claims the counter has certainly moved. - The BUILD_ASSERT also bounds the property to what cfg_len (uint8_t) holds. - The RAK4631 overlay comment now states the manual's 4.2.2 caution against DSM with VDD and VBACKUP close, and why 7.3's DSM is kept. A residual risk remains, and the earlier message overstated it as closed: a 24-series EEPROM at 0x52 whose first seven bytes leave every always-zero bit clear and whose byte 0Eh is odd still reads as a lost-power RV3028 and is adopted. The manual specifies no fixed ID value for a stronger check. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 19 +++++++++++++------ .../boards/nrf52840/rak4631/board.overlay | 5 ++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index b9203db0..7fd7bcb3 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -44,7 +44,8 @@ struct rtc_desc { IF_ENABLED(DT_NODE_HAS_PROP(node, rv3028_eeprom_config), \ (static const uint8_t RTC_CFG_NAME(node)[] = \ DT_PROP(node, rv3028_eeprom_config); \ - BUILD_ASSERT(sizeof(RTC_CFG_NAME(node)) % 3 == 0, \ + BUILD_ASSERT(sizeof(RTC_CFG_NAME(node)) % 3 == 0 && \ + sizeof(RTC_CFG_NAME(node)) <= UINT8_MAX, \ "rv3028-eeprom-config is (register, mask, value) triplets");)) DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_CFG_ARRAY) @@ -71,8 +72,10 @@ static const struct rtc_desc rtc_descs[] = { /* Chip we'll read/write going forward (first one found present). */ static const struct rtc_desc *s_active; static bool s_probed; -/* A candidate read all 0xFF and was skipped; see zephcore_rtc_save(). */ +/* A candidate read all 0xFF and was skipped; zephcore_rtc_save() probes + * once more for it, and only once per boot. */ static bool s_skipped_ff; +static bool s_reprobed; #define BCD2BIN(x) ((((x) >> 4) & 0x0F) * 10 + ((x) & 0x0F)) @@ -205,7 +208,7 @@ static bool rv3028_time_bits_ok(const uint8_t blk[7]) * automatic refresh (manual 4.6.3, 4.6.9), refresh RAM from EEPROM so the * comparison is against what is stored (4.6.4, as Zephyr's driver does), * write only what differs, commit it with the Update command (4.6.3), and - * read it back (ours). Refresh is always re-enabled. A triplet outside + * refresh again and read it back (ours). Refresh is always re-enabled. A triplet outside * 35h-37h, masking an unimplemented bit, or repeating a register is * ignored, so a devicetree mistake cannot force an Update on every boot. */ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) @@ -239,7 +242,10 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) } } if (ok && changed) { - ok = rv3028_eeprom_cmd(d, RV3028_EE_CMD_UPDATE, 10); + /* Update, then Refresh, so the read-back below sees what the + * EEPROM holds rather than what was written to RAM. */ + ok = rv3028_eeprom_cmd(d, RV3028_EE_CMD_UPDATE, 10) && + rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH, 1); } for (size_t i = 0; ok && changed && i + 2 < d->cfg_len; i += 3) { uint8_t now; @@ -368,9 +374,10 @@ void zephcore_rtc_save(uint32_t epoch) (void)rtc_probe(NULL); s_probed = true; } - if (s_active == NULL && s_skipped_ff) { - /* An RTC's counter has moved its time off all 0xFF by now; an + if (s_active == NULL && s_skipped_ff && !s_reprobed) { + /* A running RTC may have counted off all 0xFF since boot; an * erased EEPROM has not, and is skipped again. */ + s_reprobed = true; (void)rtc_probe(NULL); } if (s_active == NULL) { diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index 8bf0b70e..de80c3a1 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -231,7 +231,10 @@ /* The RAK12002's backup is a supercapacitor. As the RV-3028-C7 * manual's rechargeable-backup circuit (7.3): direct switchover * with trickle charging (0x37 TCE, BSM), TCR at its 3 kOhm delivery - * default; CLKOUT off (0x35 CLKOE), as it is unused. */ + * default; CLKOUT off (0x35 CLKOE), as it is unused. 4.2.2 cautions + * that DSM can switch needlessly when VDD and VBACKUP are close; 7.3 + * prescribes it for a capacitor anyway, and level switching would + * leave the charge below 2.0 V unused. */ rv3028-eeprom-config = [35 80 00 37 2f 24]; }; }; From 9e9452fb2a9e35003ec8d7c017b11c7bd693adb4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 16:59:52 -0700 Subject: [PATCH 09/26] rtc: do not spend the re-probe in the call that made the first probe When restore never ran, zephcore_rtc_save() probed and then, in the same call, re-probed straight away for an all-0xFF candidate, too soon for an RTC to have counted, and used up the once-per-boot re-probe. The re-probe now runs only on a later save. The flag's comment now says the re-probe runs the whole probe, not only for the skipped candidate. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 7fd7bcb3..006fb6be 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -72,8 +72,8 @@ static const struct rtc_desc rtc_descs[] = { /* Chip we'll read/write going forward (first one found present). */ static const struct rtc_desc *s_active; static bool s_probed; -/* A candidate read all 0xFF and was skipped; zephcore_rtc_save() probes - * once more for it, and only once per boot. */ +/* A candidate read all 0xFF and was skipped: a later zephcore_rtc_save() + * runs the whole probe once more, once per boot. */ static bool s_skipped_ff; static bool s_reprobed; @@ -373,8 +373,7 @@ void zephcore_rtc_save(uint32_t epoch) /* Restore wasn't run (unexpected) — discover now. */ (void)rtc_probe(NULL); s_probed = true; - } - if (s_active == NULL && s_skipped_ff && !s_reprobed) { + } else if (s_active == NULL && s_skipped_ff && !s_reprobed) { /* A running RTC may have counted off all 0xFF since boot; an * erased EEPROM has not, and is skipped again. */ s_reprobed = true; From d2615c336f4aae22705e16530bf3300b2387359a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 18:06:11 -0700 Subject: [PATCH 10/26] rtc: state the probe's EEPROM write and re-probe in the header contract From the local pass over 9e9452f: - ZephyrRTCDiscover.h said candidates come only from rtc-i2c.dtsi and that any present chip becomes the write-back target. It now says a board overlay can declare one, which chips are adopted (never an all-0xFF block), that adopting one with rv3028-eeprom-config can write its EEPROM (about 70 ms), and that a save can run the probe once more, in the caller's context, which for ZephyrRTCClock is the system work queue. - A probe run from zephcore_rtc_save() logged "RTC ...: restored" for a time it did not restore, as the restore-never-ran path did before the re-probe existed. Only the restore path logs it now. Two earlier messages on this branch are inaccurate, and the squashed upstream commit replaces both: - 9d52ff4 understates the residual risk. A 24-series EEPROM at 0x52 whose time-block bytes leave every always-zero bit clear is adopted if those seven bytes are valid BCD, whatever 0Eh holds, and also if they are not but 0Eh bit 0 is set. Odd 0Eh is the second case only. - 778a9e3 says the RV3028's time registers reset to 0. Date and Month reset to 01h (manual 3.18). Its conclusion, that a powered RV3028 never reads all 0xFF from reset, still holds. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 4 ++-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 006fb6be..b2bf8ea8 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -353,9 +353,9 @@ static bool rtc_probe(uint32_t *epoch_out) 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); } - LOG_INF("RTC %s: restored %04u-%02u-%02u %02u:%02u:%02u UTC", - d->name, year, month, day, hour, min, sec); return true; } return false; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 4a338625..3e9944ba 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -4,8 +4,9 @@ * 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. + * (boards/common/rtc-i2c.dtsi, or a board's own overlay). 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). Every @@ -25,9 +26,11 @@ extern "C" { /* * 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. + * *epoch_out and return true. The first chip that reads as an RTC (valid BCD + * time, or its power-loss flag set; never all 0xFF) is remembered as the + * write-back target, valid time or not. If its descriptor carries + * rv3028-eeprom-config, adopting it can write the chip's EEPROM (about 70 ms). + * Returns false if none present or no trustworthy time is held. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -35,6 +38,8 @@ 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. + * If restore adopted nothing but skipped an all-0xFF candidate, the first + * save probes again, once per boot, in the caller's context. */ void zephcore_rtc_save(uint32_t epoch); From 041ec790f45fcf6d71d1a409d25f2ed7a6714079 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 18:07:26 -0700 Subject: [PATCH 11/26] rak4631: keep the RV3028's FEDE bit set The 37h triplet's mask 0x2F left FEDE (bit 4) as the chip held it. FEDE is 1 on delivery and the manual's 3.15.6 says it "should always be set to 1": it lets the switchover logic follow a fast VDD edge. A chip that other firmware left with FEDE clear kept it clear. Mask 0x3F, value 0x34 now set it, which costs no EEPROM write on a chip that still has the delivery value. Raised by the MeshCore session working the same RV3028 fix there. Co-Authored-By: Claude Opus 5.5 --- zephcore/boards/nrf52840/rak4631/board.overlay | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index de80c3a1..65359a1f 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -231,11 +231,12 @@ /* The RAK12002's backup is a supercapacitor. As the RV-3028-C7 * manual's rechargeable-backup circuit (7.3): direct switchover * with trickle charging (0x37 TCE, BSM), TCR at its 3 kOhm delivery - * default; CLKOUT off (0x35 CLKOE), as it is unused. 4.2.2 cautions + * default, FEDE set as 3.15.6 says it always should be; CLKOUT off + * (0x35 CLKOE), as it is unused. 4.2.2 cautions * that DSM can switch needlessly when VDD and VBACKUP are close; 7.3 * prescribes it for a capacitor anyway, and level switching would * leave the charge below 2.0 V unused. */ - rv3028-eeprom-config = [35 80 00 37 2f 24]; + rv3028-eeprom-config = [35 80 00 37 3f 34]; }; }; From 7b6a82a336ff3ec788d814e41663a4902ade56ce Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 18:28:48 -0700 Subject: [PATCH 12/26] rtc: disable backup switchover while touching the RV3028's EEPROM The RV-3028-C7 manual's 3.15.6 says BSM must be 00 or 10 "to read/write from/to the EEPROM", and 4.6.8 that a write which loses VDD is corrupted. The config step ran its Refresh on every boot, and its one Update, with direct switchover live. On the first boot it wrote BSM = 01 into RAM just before the Update. On the RAK12002 the trickle-charged supercap sits about 0.25 V under VDD, the case 4.2.2 warns can switch needlessly, and an Update rewrites all of 30h-37h, the factory trim included. The step now holds automatic refresh off, saves 37h and clears its BSM in RAM, then reads each configured byte from the EEPROM with the single-byte command (4.6.6) and writes it with the single-byte command (4.6.5) only if it differs. A closing Refresh restores RAM, and with it the stored BSM, from the EEPROM, and the written bytes are read back. If the Refresh did not complete, the saved 37h is written back so switchover is not left off. EEbusy is polled every 1 ms instead of 10 ms, and EERD is now set before EEbusy is checked, in the order 4.6.7 gives. The header no longer says adoption blocks for about 70 ms, which was wrong even before this change. Zephyr's drivers/mfd/mfd_rv3028.c and drivers/eeprom/eeprom_rv3028.c also access the EEPROM with switchover live. Tested on a RAK4631 + RAK12002, repeater debug build: - A build with 37h TCR = 01 logged "config committed to EEPROM" and restored the time; this build then committed TCR = 00 back. - A reboot logged no config line (nothing differed) and restored. - 31 s with USB unplugged (no battery fitted): restored, power-loss flag clear, so switchover was enabled again after the config step. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 83 +++++++++++++-------- zephcore/adapters/clock/ZephyrRTCDiscover.h | 3 +- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index b2bf8ea8..4a5afb11 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -79,17 +79,20 @@ static bool s_reprobed; #define BCD2BIN(x) ((((x) >> 4) & 0x0F) * 10 + ((x) & 0x0F)) -/* RV3028 registers for committing configuration to its EEPROM, as Zephyr's - * drivers/mfd/mfd_rv3028.c does. */ +/* RV3028 registers for reading and writing its configuration EEPROM. */ #define RV3028_REG_STATUS 0x0E #define RV3028_STATUS_EEBUSY BIT(7) #define RV3028_REG_CONTROL1 0x0F #define RV3028_CONTROL1_EERD BIT(3) +#define RV3028_REG_EE_ADDR 0x25 +#define RV3028_REG_EE_DATA 0x26 #define RV3028_REG_EE_COMMAND 0x27 -#define RV3028_EE_CMD_UPDATE 0x11 /* all configuration RAM -> EEPROM */ #define RV3028_EE_CMD_REFRESH 0x12 /* all configuration EEPROM -> RAM */ +#define RV3028_EE_CMD_WRITE 0x21 /* EEDATA -> one EEPROM byte */ +#define RV3028_EE_CMD_READ 0x22 /* one EEPROM byte -> EEDATA */ #define RV3028_REG_CFG_FIRST 0x35 /* EEPROM Clkout */ #define RV3028_REG_CFG_LAST 0x37 /* EEPROM Backup */ +#define RV3028_BACKUP_BSM 0x0C /* 37h switchover mode; 00 = disabled */ #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 @@ -143,23 +146,23 @@ static bool rtc_time_unreliable(const struct rtc_desc *d, const uint8_t blk[7]) return (st & d->status_mask) != 0; } -/* Wait for EEbusy to clear: ~66 ms after power-on, ~63 ms for an Update. */ +/* Wait for EEbusy to clear: ~66 ms after power-on, ~16 ms for a byte write. */ static bool rv3028_eeprom_idle(const struct rtc_desc *d) { - for (int ms = 0; ms <= 100; ms += 10) { + for (int ms = 0; ms <= 100; ms++) { uint8_t st; if (i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) == 0 && !(st & RV3028_STATUS_EEBUSY)) { return true; } - k_msleep(10); + k_msleep(1); } return false; } -/* The manual (4.6.7): wait 10 ms after an EEPROM write or Update, 1 ms after - * a read or Refresh, before checking EEbusy. */ +/* The manual (4.6.7): wait 10 ms after an EEPROM write, 1 ms after a read or + * Refresh, before checking EEbusy. */ static bool rv3028_eeprom_cmd(const struct rtc_desc *d, uint8_t cmd, int32_t wait_ms) { if (i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, 0x00) != 0 || @@ -170,6 +173,21 @@ static bool rv3028_eeprom_cmd(const struct rtc_desc *d, uint8_t cmd, int32_t wai return rv3028_eeprom_idle(d); } +/* One EEPROM byte, read (4.6.6) or written (4.6.5). */ +static bool rv3028_eeprom_read(const struct rtc_desc *d, uint8_t reg, uint8_t *val) +{ + return i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_ADDR, reg) == 0 && + rv3028_eeprom_cmd(d, RV3028_EE_CMD_READ, 1) && + i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_EE_DATA, val) == 0; +} + +static bool rv3028_eeprom_write(const struct rtc_desc *d, uint8_t reg, uint8_t val) +{ + return i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_ADDR, reg) == 0 && + i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_DATA, val) == 0 && + rv3028_eeprom_cmd(d, RV3028_EE_CMD_WRITE, 10); +} + /* A config triplet is used only if its register is 35h-37h, its mask covers * implemented bits only (35h bits 5:4 are not; manual 3.15.4), and no * earlier triplet names the same register. */ @@ -204,27 +222,31 @@ static bool rv3028_time_bits_ok(const uint8_t blk[7]) return true; } -/* Apply the descriptor's rv3028-eeprom-config to the adopted chip: hold off - * automatic refresh (manual 4.6.3, 4.6.9), refresh RAM from EEPROM so the - * comparison is against what is stored (4.6.4, as Zephyr's driver does), - * write only what differs, commit it with the Update command (4.6.3), and - * refresh again and read it back (ours). Refresh is always re-enabled. A triplet outside - * 35h-37h, masking an unimplemented bit, or repeating a register is - * ignored, so a devicetree mistake cannot force an Update on every boot. */ +/* Apply the descriptor's rv3028-eeprom-config to the adopted chip. The + * EEPROM is only touched with automatic refresh held off (4.6.7) and backup + * switchover disabled in RAM (3.15.6: BSM 00 for any EEPROM read or write). + * Each byte is compared against the EEPROM itself (4.6.6) and written only if + * it differs (4.6.5). A closing Refresh (4.6.4) reloads RAM from the EEPROM, + * switching back to the stored BSM, and what was written is read back. A + * triplet outside 35h-37h, masking an unimplemented bit, or repeating a + * register is ignored, so a devicetree mistake cannot write on every boot. */ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) { - bool changed = false, ok = true; + bool changed = false, held = false, refreshed = false, ok; + uint8_t backup = 0; if (d->cfg == NULL) { return; } - if (!rv3028_eeprom_idle(d) || - i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, - RV3028_CONTROL1_EERD, RV3028_CONTROL1_EERD) != 0) { - LOG_WRN("%s: config not applied", d->name); - return; + ok = i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, + RV3028_CONTROL1_EERD, RV3028_CONTROL1_EERD) == 0 && + rv3028_eeprom_idle(d) && + i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, &backup) == 0; + if (ok) { + held = true; + ok = i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, + backup & ~RV3028_BACKUP_BSM) == 0; } - ok = rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH, 1); for (size_t i = 0; ok && i + 2 < d->cfg_len; i += 3) { uint8_t old, reg = d->cfg[i], mask = d->cfg[i + 1]; @@ -234,18 +256,14 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) LOG_WRN("%s: config triplet for 0x%02x ignored", d->name, reg); continue; } - ok = i2c_reg_read_byte(d->bus, d->addr, reg, &old) == 0; + ok = rv3028_eeprom_read(d, reg, &old); if (ok && ((old ^ val) & mask) != 0) { - ok = i2c_reg_write_byte(d->bus, d->addr, reg, - (old & ~mask) | (val & mask)) == 0; + ok = rv3028_eeprom_write(d, reg, (old & ~mask) | (val & mask)); changed = true; } } - if (ok && changed) { - /* Update, then Refresh, so the read-back below sees what the - * EEPROM holds rather than what was written to RAM. */ - ok = rv3028_eeprom_cmd(d, RV3028_EE_CMD_UPDATE, 10) && - rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH, 1); + if (ok) { + ok = refreshed = rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH, 1); } for (size_t i = 0; ok && changed && i + 2 < d->cfg_len; i += 3) { uint8_t now; @@ -257,6 +275,11 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) ((now ^ d->cfg[i + 2]) & d->cfg[i + 1]) == 0; } + /* The Refresh did not run or did not finish: put the switchover mode + * back as it was rather than leave it disabled. */ + if (held && !refreshed) { + (void)i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, backup); + } if (i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, RV3028_CONTROL1_EERD, 0) != 0) { ok = false; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 3e9944ba..012c5547 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -29,7 +29,8 @@ extern "C" { * *epoch_out and return true. The first chip that reads as an RTC (valid BCD * time, or its power-loss flag set; never all 0xFF) is remembered as the * write-back target, valid time or not. If its descriptor carries - * rv3028-eeprom-config, adopting it can write the chip's EEPROM (about 70 ms). + * rv3028-eeprom-config, adopting it reads the chip's EEPROM (a few ms) and + * writes each byte that differs (about 16 ms each). * Returns false if none present or no trustworthy time is held. */ bool zephcore_rtc_restore(uint32_t *epoch_out); From 46bfd7c679da3e2fe42e0e0cbec923e139078c18 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 19:36:52 -0700 Subject: [PATCH 13/26] rtc: retry what a failed config step left off, and say so From the local pass over 7b6a82a: - A config step that failed after clearing BSM restored 37h with one unchecked write, then cleared EERD. A bus fault lasting through both left backup switchover off and automatic refresh held, so not even the midnight refresh put BSM back: a power cut before the next boot lost the time, and the only log was "config not applied". What could not be put back is now kept, retried on every save, and logged as an error until it is. - The header's restore contract did not say that a chip at an RV3028 descriptor is refused when an always-zero bit reads 1, nor that adoption turns switchover off while it reads the EEPROM, and can wait up to ~66 ms more behind the chip's power-on refresh. Checked on the RAK4631 + RAK12002: flash and reboot each restored the time with no config line and no warning. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 30 ++++++++++++++++++--- zephcore/adapters/clock/ZephyrRTCDiscover.h | 10 ++++--- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 4a5afb11..47452ad1 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -76,6 +76,10 @@ static bool s_probed; * runs the whole probe once more, once per boot. */ static bool s_skipped_ff; static bool s_reprobed; +/* What a failed config step could not put back on the RV3028, retried on + * each save: the saved 37h (switchover mode) and automatic refresh. */ +static bool s_bsm_pending, s_eerd_pending; +static uint8_t s_bsm_saved; #define BCD2BIN(x) ((((x) >> 4) & 0x0F) * 10 + ((x) & 0x0F)) @@ -222,6 +226,20 @@ static bool rv3028_time_bits_ok(const uint8_t blk[7]) return true; } +static bool rv3028_release(const struct rtc_desc *d) +{ + if (s_bsm_pending && + i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, s_bsm_saved) == 0) { + s_bsm_pending = false; + } + if (s_eerd_pending && + i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, + RV3028_CONTROL1_EERD, 0) == 0) { + s_eerd_pending = false; + } + return !s_bsm_pending && !s_eerd_pending; +} + /* Apply the descriptor's rv3028-eeprom-config to the adopted chip. The * EEPROM is only touched with automatic refresh held off (4.6.7) and backup * switchover disabled in RAM (3.15.6: BSM 00 for any EEPROM read or write). @@ -238,6 +256,7 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) if (d->cfg == NULL) { return; } + s_eerd_pending = true; ok = i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, RV3028_CONTROL1_EERD, RV3028_CONTROL1_EERD) == 0 && rv3028_eeprom_idle(d) && @@ -278,10 +297,11 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) /* The Refresh did not run or did not finish: put the switchover mode * back as it was rather than leave it disabled. */ if (held && !refreshed) { - (void)i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, backup); + s_bsm_saved = backup; + s_bsm_pending = true; } - if (i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, - RV3028_CONTROL1_EERD, 0) != 0) { + if (!rv3028_release(d)) { + LOG_ERR("%s: switchover or auto refresh left off, retried on save", d->name); ok = false; } if (!ok) { @@ -407,6 +427,10 @@ void zephcore_rtc_save(uint32_t epoch) } const struct rtc_desc *d = s_active; + + if ((s_bsm_pending || s_eerd_pending) && rv3028_release(d)) { + LOG_INF("%s: switchover and auto refresh restored", d->name); + } int y; unsigned m, day; civil_from_days((int64_t)epoch / 86400, &y, &m, &day); diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 012c5547..f98ad3e8 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -27,10 +27,12 @@ extern "C" { * 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 first chip that reads as an RTC (valid BCD - * time, or its power-loss flag set; never all 0xFF) is remembered as the - * write-back target, valid time or not. If its descriptor carries - * rv3028-eeprom-config, adopting it reads the chip's EEPROM (a few ms) and - * writes each byte that differs (about 16 ms each). + * time, or its power-loss flag set; never all 0xFF, nor, at a descriptor with + * rv3028-eeprom-config, with an RV3028 always-zero bit set) is remembered as + * the write-back target, valid time or not. Adopting one with + * rv3028-eeprom-config turns its backup switchover off while it reads the + * EEPROM and refreshes RAM from it (a few ms, up to ~66 ms more during the + * chip's power-on refresh) and writes each byte that differs (~16 ms each). * Returns false if none present or no trustworthy time is held. */ bool zephcore_rtc_restore(uint32_t *epoch_out); From e5a8d0af9834bbdd5ab55107f7b278fa498a7d66 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 19:46:17 -0700 Subject: [PATCH 14/26] rtc: re-run an incomplete RV3028 config step instead of a saved 37h From the local pass over 46bfd7c, which found the retry it added wrong: - The retry wrote back the 37h RAM held before the step. If the step's EEPROM write had landed, a later Refresh (the step's own, finishing after its EEbusy poll failed, or the midnight one) loaded the new 37h, and the retry then overwrote it with the old value, turning switchover off for up to a day. A save now re-runs the whole config step instead, which reads the EEPROM afresh, so it cannot restore a stale value, and holds switchover off for its EEPROM access as the first run did. - Inside the step, a Refresh that did not complete is tried once more, which restores the stored switchover mode and cannot be stale; only if that fails too is the pre-step 37h written back. - A failed retry logged nothing after boot. Each failed run now logs "config not applied, retried on the next save". - The header's save contract now says a save re-runs an incomplete config step. Saves follow GPS, app and CLI syncs, so without one the step waits for the next boot; with EERD clear, the midnight refresh restores switchover from the EEPROM in the meantime. 46bfd7c's message claimed a failure is "logged as an error until it is" put back; that was not true of the code it carried. Checked on the RAK4631 + RAK12002: flash and reboot each restored the time with no config line and no warning. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 46 +++++++-------------- zephcore/adapters/clock/ZephyrRTCDiscover.h | 3 +- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 47452ad1..b6130103 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -76,10 +76,8 @@ static bool s_probed; * runs the whole probe once more, once per boot. */ static bool s_skipped_ff; static bool s_reprobed; -/* What a failed config step could not put back on the RV3028, retried on - * each save: the saved 37h (switchover mode) and automatic refresh. */ -static bool s_bsm_pending, s_eerd_pending; -static uint8_t s_bsm_saved; +/* The RV3028 config step did not complete: each save runs it again. */ +static bool s_cfg_pending; #define BCD2BIN(x) ((((x) >> 4) & 0x0F) * 10 + ((x) & 0x0F)) @@ -226,20 +224,6 @@ static bool rv3028_time_bits_ok(const uint8_t blk[7]) return true; } -static bool rv3028_release(const struct rtc_desc *d) -{ - if (s_bsm_pending && - i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, s_bsm_saved) == 0) { - s_bsm_pending = false; - } - if (s_eerd_pending && - i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, - RV3028_CONTROL1_EERD, 0) == 0) { - s_eerd_pending = false; - } - return !s_bsm_pending && !s_eerd_pending; -} - /* Apply the descriptor's rv3028-eeprom-config to the adopted chip. The * EEPROM is only touched with automatic refresh held off (4.6.7) and backup * switchover disabled in RAM (3.15.6: BSM 00 for any EEPROM read or write). @@ -256,7 +240,6 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) if (d->cfg == NULL) { return; } - s_eerd_pending = true; ok = i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, RV3028_CONTROL1_EERD, RV3028_CONTROL1_EERD) == 0 && rv3028_eeprom_idle(d) && @@ -294,18 +277,20 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) ((now ^ d->cfg[i + 2]) & d->cfg[i + 1]) == 0; } - /* The Refresh did not run or did not finish: put the switchover mode - * back as it was rather than leave it disabled. */ - if (held && !refreshed) { - s_bsm_saved = backup; - s_bsm_pending = true; + /* The Refresh did not run or did not finish: try it once more, as it + * also restores the stored switchover mode, else put back the mode the + * chip had rather than leave switchover disabled. */ + if (held && !refreshed && + !rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH, 1)) { + (void)i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, backup); } - if (!rv3028_release(d)) { - LOG_ERR("%s: switchover or auto refresh left off, retried on save", d->name); + if (i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, + RV3028_CONTROL1_EERD, 0) != 0) { ok = false; } + s_cfg_pending = !ok; if (!ok) { - LOG_WRN("%s: config not applied", d->name); + LOG_WRN("%s: config not applied, retried on the next save", d->name); } else if (changed) { LOG_INF("%s: config committed to EEPROM", d->name); } @@ -412,6 +397,9 @@ bool zephcore_rtc_restore(uint32_t *epoch_out) void zephcore_rtc_save(uint32_t epoch) { + if (s_active != NULL && s_cfg_pending) { + rv3028_apply_eeprom_config(s_active); + } if (!s_probed) { /* Restore wasn't run (unexpected) — discover now. */ (void)rtc_probe(NULL); @@ -427,10 +415,6 @@ void zephcore_rtc_save(uint32_t epoch) } const struct rtc_desc *d = s_active; - - if ((s_bsm_pending || s_eerd_pending) && rv3028_release(d)) { - LOG_INF("%s: switchover and auto refresh restored", d->name); - } int y; unsigned m, day; civil_from_days((int64_t)epoch / 86400, &y, &m, &day); diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index f98ad3e8..146b6caf 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -42,7 +42,8 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * 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. * If restore adopted nothing but skipped an all-0xFF candidate, the first - * save probes again, once per boot, in the caller's context. + * save probes again, once per boot, in the caller's context. If an RV3028's + * config step did not complete, each save runs it again until it does. */ void zephcore_rtc_save(uint32_t epoch); From cd5b5e3a34e716ea2a901dd4c5811aaba1c13310 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 3 Oct 2026 19:59:43 -0700 Subject: [PATCH 15/26] rtc: write the configured 37h on a failed step, cap its re-runs From the local pass over e5a8d0a. The maintainer chose to simplify: - The in-step second Refresh was sent without checking EEbusy, so a chip still busy with an earlier command ignored it and the code took it as done, leaving BSM 00. It is gone. - The fallback wrote the 37h RAM held before the step, which is stale once the step's EEPROM write has landed and been refreshed into RAM; e5a8d0a's message claimed otherwise. The fallback now writes 37h with the configured bits applied, as MeshCore's fix does. It is a RAM write, so the EEPROM and 3.15.6 are not involved, and it never puts back an older value. - A step that can never complete (a password-locked RV3028, or an EEPROM impostor that passes the always-zero check) re-ran on every save. A save now re-runs it at most three times per boot. Checked on the RAK4631 + RAK12002: flash and reboot each restored the time with no config line and no warning. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 32 +++++++++++++++------ zephcore/adapters/clock/ZephyrRTCDiscover.h | 2 +- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index b6130103..8dbf3a61 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -76,8 +76,11 @@ static bool s_probed; * runs the whole probe once more, once per boot. */ static bool s_skipped_ff; static bool s_reprobed; -/* The RV3028 config step did not complete: each save runs it again. */ +/* The RV3028 config step did not complete: a save runs it again, up to + * RTC_CFG_RERUNS times per boot. */ +#define RTC_CFG_RERUNS 3 static bool s_cfg_pending; +static uint8_t s_cfg_reruns; #define BCD2BIN(x) ((((x) >> 4) & 0x0F) * 10 + ((x) & 0x0F)) @@ -224,6 +227,17 @@ static bool rv3028_time_bits_ok(const uint8_t blk[7]) return true; } +/* RAM 37h with the configured 37h bits applied, or as it was if none are. */ +static uint8_t rv3028_cfg_backup_ram(const struct rtc_desc *d, uint8_t ram) +{ + for (size_t i = 0; i + 2 < d->cfg_len; i += 3) { + if (d->cfg[i] == RV3028_REG_CFG_LAST && rv3028_cfg_triplet_ok(d, i)) { + return (ram & ~d->cfg[i + 1]) | (d->cfg[i + 2] & d->cfg[i + 1]); + } + } + return ram; +} + /* Apply the descriptor's rv3028-eeprom-config to the adopted chip. The * EEPROM is only touched with automatic refresh held off (4.6.7) and backup * switchover disabled in RAM (3.15.6: BSM 00 for any EEPROM read or write). @@ -277,12 +291,11 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) ((now ^ d->cfg[i + 2]) & d->cfg[i + 1]) == 0; } - /* The Refresh did not run or did not finish: try it once more, as it - * also restores the stored switchover mode, else put back the mode the - * chip had rather than leave switchover disabled. */ - if (held && !refreshed && - !rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH, 1)) { - (void)i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, backup); + /* The Refresh did not run or did not finish, so RAM may still hold BSM + * 00. Write 37h as configured: RAM only, and never an older value. */ + if (held && !refreshed) { + (void)i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, + rv3028_cfg_backup_ram(d, backup)); } if (i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, RV3028_CONTROL1_EERD, 0) != 0) { @@ -290,7 +303,7 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) } s_cfg_pending = !ok; if (!ok) { - LOG_WRN("%s: config not applied, retried on the next save", d->name); + LOG_WRN("%s: config not applied", d->name); } else if (changed) { LOG_INF("%s: config committed to EEPROM", d->name); } @@ -397,7 +410,8 @@ bool zephcore_rtc_restore(uint32_t *epoch_out) void zephcore_rtc_save(uint32_t epoch) { - if (s_active != NULL && s_cfg_pending) { + if (s_active != NULL && s_cfg_pending && s_cfg_reruns < RTC_CFG_RERUNS) { + s_cfg_reruns++; rv3028_apply_eeprom_config(s_active); } if (!s_probed) { diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 146b6caf..3ea1f76e 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -43,7 +43,7 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * intended only for real syncs (GPS/app/CLI), not per-packet clock nudges. * If restore adopted nothing but skipped an all-0xFF candidate, the first * save probes again, once per boot, in the caller's context. If an RV3028's - * config step did not complete, each save runs it again until it does. + * config step did not complete, a save runs it again, at most 3 per boot. */ void zephcore_rtc_save(uint32_t epoch); From 4ce81ba5baea7c3d0281a09e5150807b9398f90e Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 4 Oct 2026 07:29:27 -0700 Subject: [PATCH 16/26] rtc: close #35's open list, matching MeshCore's RV3028 store Closes the failure-path items #35 listed as open, in the shape the MeshCore fix (ptr727/meshcore-dev-MeshCore#11, 037d493b) settled on, so the two behave alike: - EEbusy wait: a failed status read ends it at once. With this board's 1000 ms I2C transfer timeout, polling through failures could block about 100 s at boot or on the system work queue. - Read-back: runs after every Refresh, not only when a byte was written, so every boot confirms the stored switchover mode came back. - Fallback: when the Refresh did not complete, 37h is written as configured only after a best-effort wait for EEbusy, so an EEPROM operation still running normally finishes first. - Any failed store, including one whose Refresh completed but whose read-back did not match, now sets the config in RAM (masked read-modify-write), so switchover is on until the chip's next daily refresh, by which time a retry has normally stored it. - Retry: a delayed work item re-runs the store every 10 minutes, at most 3 times per boot, instead of only on a save, which a node without GPS, app or CLI syncs never makes. Each failed attempt logs whether the RAM fallback took. - Identity: an RV3028 always-zero bit must show on two reads before a chip at an RV3028 descriptor is refused, so one corrupted read does not cost a real RV3028 its adoption. The header and binding now state the two-read identity check, the RAM fallback and its lifetime, and that the power-on refresh is waited out before switchover is turned off rather than inside that window. Still open, as in MeshCore: a password-locked chip can report success with nothing written (manual 4.18.1), and the switchover-off window is estimated, not measured. Co-Authored-By: Claude Opus 5.5 --- zephcore/Kconfig | 3 +- zephcore/adapters/clock/ZephyrRTCDiscover.c | 137 ++++++++++++------ zephcore/adapters/clock/ZephyrRTCDiscover.h | 16 +- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 12 +- 4 files changed, 113 insertions(+), 55 deletions(-) diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 4119ff24..9ad85d14 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -904,7 +904,8 @@ config ZEPHCORE_RTC_AUTODISCOVER shown tagged "L" until the next external sync. On every GPS/app/CLI sync the time is written back so it survives power-off. A node with rv3028-eeprom-config also has that configuration committed to the - chip's EEPROM when it differs. Compact raw-I2C reader (no Zephyr RTC + chip's EEPROM when it differs, with backup switchover held off while + the EEPROM is accessed. Compact raw-I2C reader (no Zephyr RTC subsystem). Safe to leave on: a board that declares no zephcore,rtc-i2c node compiles to nothing. diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 8dbf3a61..904d5b8d 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -76,11 +76,6 @@ static bool s_probed; * runs the whole probe once more, once per boot. */ static bool s_skipped_ff; static bool s_reprobed; -/* The RV3028 config step did not complete: a save runs it again, up to - * RTC_CFG_RERUNS times per boot. */ -#define RTC_CFG_RERUNS 3 -static bool s_cfg_pending; -static uint8_t s_cfg_reruns; #define BCD2BIN(x) ((((x) >> 4) & 0x0F) * 10 + ((x) & 0x0F)) @@ -151,14 +146,18 @@ static bool rtc_time_unreliable(const struct rtc_desc *d, const uint8_t blk[7]) return (st & d->status_mask) != 0; } -/* Wait for EEbusy to clear: ~66 ms after power-on, ~16 ms for a byte write. */ +/* Wait for EEbusy to clear: ~66 ms after power-on, ~16 ms for a byte write. + * A failed read ends the wait, rather than polling a dead bus through each + * transfer's timeout. */ static bool rv3028_eeprom_idle(const struct rtc_desc *d) { for (int ms = 0; ms <= 100; ms++) { uint8_t st; - if (i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) == 0 && - !(st & RV3028_STATUS_EEBUSY)) { + if (i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) != 0) { + return false; + } + if (!(st & RV3028_STATUS_EEBUSY)) { return true; } k_msleep(1); @@ -238,22 +237,20 @@ static uint8_t rv3028_cfg_backup_ram(const struct rtc_desc *d, uint8_t ram) return ram; } -/* Apply the descriptor's rv3028-eeprom-config to the adopted chip. The +/* Store the descriptor's rv3028-eeprom-config in the chip's EEPROM. The * EEPROM is only touched with automatic refresh held off (4.6.7) and backup * switchover disabled in RAM (3.15.6: BSM 00 for any EEPROM read or write). * Each byte is compared against the EEPROM itself (4.6.6) and written only if * it differs (4.6.5). A closing Refresh (4.6.4) reloads RAM from the EEPROM, - * switching back to the stored BSM, and what was written is read back. A - * triplet outside 35h-37h, masking an unimplemented bit, or repeating a - * register is ignored, so a devicetree mistake cannot write on every boot. */ -static void rv3028_apply_eeprom_config(const struct rtc_desc *d) + * switching back to the stored BSM, and the config is read back on every run. + * A triplet outside 35h-37h, masking an unimplemented bit, or repeating a + * register is ignored, so a devicetree mistake cannot write on every boot. + * Returns false if the config was not confirmed, or EERD was not cleared. */ +static bool rv3028_store_config(const struct rtc_desc *d) { bool changed = false, held = false, refreshed = false, ok; uint8_t backup = 0; - if (d->cfg == NULL) { - return; - } ok = i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, RV3028_CONTROL1_EERD, RV3028_CONTROL1_EERD) == 0 && rv3028_eeprom_idle(d) && @@ -266,34 +263,33 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) for (size_t i = 0; ok && i + 2 < d->cfg_len; i += 3) { uint8_t old, reg = d->cfg[i], mask = d->cfg[i + 1]; - uint8_t val = d->cfg[i + 2]; + uint8_t val = d->cfg[i + 2] & mask; if (!rv3028_cfg_triplet_ok(d, i)) { LOG_WRN("%s: config triplet for 0x%02x ignored", d->name, reg); continue; } ok = rv3028_eeprom_read(d, reg, &old); - if (ok && ((old ^ val) & mask) != 0) { - ok = rv3028_eeprom_write(d, reg, (old & ~mask) | (val & mask)); + if (ok && (old & mask) != val) { + ok = rv3028_eeprom_write(d, reg, (old & ~mask) | val); changed = true; } } - if (ok) { - ok = refreshed = rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH, 1); - } - for (size_t i = 0; ok && changed && i + 2 < d->cfg_len; i += 3) { + ok = refreshed = ok && rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH, 1); + for (size_t i = 0; ok && i + 2 < d->cfg_len; i += 3) { uint8_t now; - if (!rv3028_cfg_triplet_ok(d, i)) { - continue; + if (rv3028_cfg_triplet_ok(d, i)) { + ok = i2c_reg_read_byte(d->bus, d->addr, d->cfg[i], &now) == 0 && + ((now ^ d->cfg[i + 2]) & d->cfg[i + 1]) == 0; } - ok = i2c_reg_read_byte(d->bus, d->addr, d->cfg[i], &now) == 0 && - ((now ^ d->cfg[i + 2]) & d->cfg[i + 1]) == 0; } /* The Refresh did not run or did not finish, so RAM may still hold BSM - * 00. Write 37h as configured: RAM only, and never an older value. */ + * 00. Once an EEPROM operation still running has had the chance to + * finish, write 37h as configured: RAM only, never an older value. */ if (held && !refreshed) { + (void)rv3028_eeprom_idle(d); (void)i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, rv3028_cfg_backup_ram(d, backup)); } @@ -301,12 +297,66 @@ static void rv3028_apply_eeprom_config(const struct rtc_desc *d) RV3028_CONTROL1_EERD, 0) != 0) { ok = false; } - s_cfg_pending = !ok; - if (!ok) { - LOG_WRN("%s: config not applied", d->name); - } else if (changed) { + if (ok && changed) { LOG_INF("%s: config committed to EEPROM", d->name); } + return ok; +} + +/* Set the config in the RAM mirror only, which lasts until the next refresh. */ +static bool rv3028_set_ram(const struct rtc_desc *d) +{ + bool ok = true; + + for (size_t i = 0; i + 2 < d->cfg_len; i += 3) { + uint8_t old, reg = d->cfg[i], mask = d->cfg[i + 1]; + + if (rv3028_cfg_triplet_ok(d, i)) { + ok = i2c_reg_read_byte(d->bus, d->addr, reg, &old) == 0 && + i2c_reg_write_byte(d->bus, d->addr, reg, + (old & ~mask) | (d->cfg[i + 2] & mask)) == 0 && + ok; + } + } + return ok; +} + +/* A failed store is retried on the system work queue, a few times per boot + * only: a password-locked chip, or a part at the address that is not an + * RV3028, never succeeds, and every attempt writes to it again. */ +#define RTC_CFG_RETRY K_MINUTES(10) +#define RTC_CFG_RETRIES 3 + +static uint8_t s_cfg_tries; +static void rv3028_cfg_retry_fn(struct k_work *work); +static K_WORK_DELAYABLE_DEFINE(s_cfg_retry, rv3028_cfg_retry_fn); + +/* Store the config, falling back to the RAM mirror if that fails: without + * the EEPROM store the RAM config lasts only until the next refresh, which on + * a chip still holding its delivery EEPROM turns switchover back off. */ +static void rv3028_configure(const struct rtc_desc *d) +{ + if (d->cfg == NULL) { + return; + } + s_cfg_tries++; + if (rv3028_store_config(d)) { + return; + } + (void)rv3028_eeprom_idle(d); + bool ram = rv3028_set_ram(d); + + LOG_WRN("%s: config not stored in EEPROM, %s in RAM (attempt %u)", d->name, + ram ? "set" : "NOT set", s_cfg_tries); + if (s_cfg_tries <= RTC_CFG_RETRIES) { + k_work_schedule(&s_cfg_retry, RTC_CFG_RETRY); + } +} + +static void rv3028_cfg_retry_fn(struct k_work *work) +{ + ARG_UNUSED(work); + rv3028_configure(s_active); } /* Probe all chips once; cache the first present one in s_active. If a present @@ -340,6 +390,18 @@ static bool rtc_probe(uint32_t *epoch_out) continue; } + /* At an RV3028 descriptor, a bit an RV3028 always reads as 0 marks + * some other part. Only two reads that agree rule out an RV3028. */ + if (d->cfg != NULL && !rv3028_time_bits_ok(blk)) { + if (i2c_burst_read(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) != 0) { + continue; + } + if (!rv3028_time_bits_ok(blk)) { + LOG_WRN("%s: not an RV3028, skipped", d->name); + continue; + } + } + /* Mask off flag/century bits. We trust this is a real RTC (vs. an * unrelated chip sharing the address) if EITHER the block is valid * BCD, OR the chip's power-loss flag is set — the latter is itself @@ -361,14 +423,9 @@ static bool rtc_probe(uint32_t *epoch_out) continue; /* neither valid time nor a lost-power RTC => skip */ } - if (d->cfg != NULL && !rv3028_time_bits_ok(blk)) { - LOG_WRN("%s: not an RV3028, skipped", d->name); - continue; - } - if (s_active == NULL) { s_active = d; /* RTC => our write-back target */ - rv3028_apply_eeprom_config(d); + rv3028_configure(d); } if (unreliable) { @@ -410,10 +467,6 @@ bool zephcore_rtc_restore(uint32_t *epoch_out) void zephcore_rtc_save(uint32_t epoch) { - if (s_active != NULL && s_cfg_pending && s_cfg_reruns < RTC_CFG_RERUNS) { - s_cfg_reruns++; - rv3028_apply_eeprom_config(s_active); - } if (!s_probed) { /* Restore wasn't run (unexpected) — discover now. */ (void)rtc_probe(NULL); diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 3ea1f76e..7f9091c4 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -28,11 +28,14 @@ extern "C" { * (year >= 2025 and its power-loss flag is clear), store the Unix epoch in * *epoch_out and return true. The first chip that reads as an RTC (valid BCD * time, or its power-loss flag set; never all 0xFF, nor, at a descriptor with - * rv3028-eeprom-config, with an RV3028 always-zero bit set) is remembered as - * the write-back target, valid time or not. Adopting one with - * rv3028-eeprom-config turns its backup switchover off while it reads the - * EEPROM and refreshes RAM from it (a few ms, up to ~66 ms more during the - * chip's power-on refresh) and writes each byte that differs (~16 ms each). + * rv3028-eeprom-config, with an RV3028 always-zero bit set on two reads) is + * remembered as the write-back target, valid time or not. Adopting one with + * rv3028-eeprom-config first waits out the chip's power-on refresh (up to + * ~66 ms), then turns its backup switchover off while it reads the EEPROM + * and refreshes RAM from it (a few ms) and writes each byte that differs + * (~16 ms each). If that store fails, the config is set in RAM, which lasts + * until the chip's next daily refresh, and the store is retried on the + * system work queue every 10 minutes, at most 3 times. * Returns false if none present or no trustworthy time is held. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -42,8 +45,7 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * 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. * If restore adopted nothing but skipped an all-0xFF candidate, the first - * save probes again, once per boot, in the caller's context. If an RV3028's - * config step did not complete, a save runs it again, at most 3 per boot. + * save probes again, once per boot, in the caller's context. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 084c582e..7cd0c368 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -49,10 +49,12 @@ properties: EEPROM-backed configuration registers 0x35 (CLKOUT) to 0x37 (backup switchover and trickle charger), one triplet per register, masking implemented bits only (35h bits 5:4 are not); any other triplet is - ignored. A chip - at the address whose always-zero time bits read 1 is not adopted. - When the chip is adopted, any - register whose masked bits differ is updated and committed to the - chip's EEPROM, so the chip's own refresh from EEPROM keeps it. Set it + ignored. A chip at the address whose always-zero time bits read 1 on + two reads is not adopted. When the chip is adopted, any register whose + masked bits differ is written to the chip's EEPROM, one byte at a time + with backup switchover held off, so the chip's own refresh from EEPROM + keeps it. If that fails, the config is set in the RAM mirror, which + lasts until the chip's next daily refresh, and the store is retried up + to 3 times, 10 minutes apart. Set it only on a board whose backup supply is known: trickle charging suits a supercapacitor or rechargeable cell, never a primary cell. From 334d9f723a159f6d9f4725e7986d725d3798cec1 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 4 Oct 2026 08:03:32 -0700 Subject: [PATCH 17/26] rtc: adopt an RV3028 unless both reads agree; name a stuck EERD From the local pass over 4ce81ba: - The confirming identity read refused the chip when it failed, so one corrupted read followed by a bus error still cost a real RV3028 its adoption for the boot, which is what the previous commit said it prevented. A needless DSM switchover mid-read (manual 4.2.2) can do exactly that. Now, as in MeshCore, a failed second read does not count, and a chip is refused only when the same register shows an always-zero bit on both reads; a clean second read is the one decoded. - A store whose EEPROM write, Refresh and read-back all succeeded but whose final EERD clear failed was logged as "config not stored in EEPROM", which it was, and set the RAM config needlessly. The store now reports that case apart: "config committed" is logged, the warning says EERD is still set and so the daily refresh is off, and the retry is scheduled to clear it. - The header now says an unreadable power-loss flag also counts toward adoption, which rtc_time_unreliable() has always done, and gives the switchover-off window as measured on a RAK4631 + RAK12002 with a timing build: 14.3 ms on a boot that writes nothing, 52.2 ms for two bytes, against the "a few ms" it said. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 63 +++++++++++++-------- zephcore/adapters/clock/ZephyrRTCDiscover.h | 12 ++-- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 904d5b8d..971082da 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -17,6 +17,8 @@ #include #include +#include + LOG_MODULE_REGISTER(zephcore_rtc, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); #define RTC_COMPAT zephcore_rtc_i2c @@ -212,14 +214,14 @@ static bool rv3028_cfg_triplet_ok(const struct rtc_desc *d, size_t i) return true; } -/* Bits the RV3028 always reads as 0 in its time block (manual 3.2). A chip - * at an RV3028 descriptor that sets any is not one, and is not adopted. */ -static bool rv3028_time_bits_ok(const uint8_t blk[7]) +/* Bits the RV3028 always reads as 0 in its time block (manual 3.2). False if + * one is set in the same register of both reads a and b: not an RV3028. */ +static bool rv3028_time_bits_ok(const uint8_t a[7], const uint8_t b[7]) { static const uint8_t zero[7] = { 0x80, 0x80, 0xC0, 0xF8, 0xC0, 0xE0, 0x00 }; for (size_t k = 0; k < 7; k++) { - if (blk[k] & zero[k]) { + if ((a[k] & zero[k]) && (b[k] & zero[k])) { return false; } } @@ -245,8 +247,11 @@ static uint8_t rv3028_cfg_backup_ram(const struct rtc_desc *d, uint8_t ram) * switching back to the stored BSM, and the config is read back on every run. * A triplet outside 35h-37h, masking an unimplemented bit, or repeating a * register is ignored, so a devicetree mistake cannot write on every boot. - * Returns false if the config was not confirmed, or EERD was not cleared. */ -static bool rv3028_store_config(const struct rtc_desc *d) + * Reports whether the config was confirmed, and if so whether EERD, which + * also stops the daily refresh, was cleared afterwards. */ +enum rv3028_store { RV3028_STORED, RV3028_EERD_SET, RV3028_NOT_STORED }; + +static enum rv3028_store rv3028_store_config(const struct rtc_desc *d) { bool changed = false, held = false, refreshed = false, ok; uint8_t backup = 0; @@ -293,14 +298,14 @@ static bool rv3028_store_config(const struct rtc_desc *d) (void)i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, rv3028_cfg_backup_ram(d, backup)); } - if (i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, - RV3028_CONTROL1_EERD, 0) != 0) { - ok = false; - } if (ok && changed) { LOG_INF("%s: config committed to EEPROM", d->name); } - return ok; + if (i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, + RV3028_CONTROL1_EERD, 0) != 0) { + return ok ? RV3028_EERD_SET : RV3028_NOT_STORED; + } + return ok ? RV3028_STORED : RV3028_NOT_STORED; } /* Set the config in the RAM mirror only, which lasts until the next refresh. */ @@ -340,14 +345,19 @@ static void rv3028_configure(const struct rtc_desc *d) return; } s_cfg_tries++; - if (rv3028_store_config(d)) { + switch (rv3028_store_config(d)) { + case RV3028_STORED: return; + case RV3028_EERD_SET: + LOG_WRN("%s: config stored, but EERD still set, so no daily refresh " + "(attempt %u)", d->name, s_cfg_tries); + break; + case RV3028_NOT_STORED: + (void)rv3028_eeprom_idle(d); + LOG_WRN("%s: config not stored in EEPROM, %s in RAM (attempt %u)", d->name, + rv3028_set_ram(d) ? "set" : "NOT set", s_cfg_tries); + break; } - (void)rv3028_eeprom_idle(d); - bool ram = rv3028_set_ram(d); - - LOG_WRN("%s: config not stored in EEPROM, %s in RAM (attempt %u)", d->name, - ram ? "set" : "NOT set", s_cfg_tries); if (s_cfg_tries <= RTC_CFG_RETRIES) { k_work_schedule(&s_cfg_retry, RTC_CFG_RETRY); } @@ -391,14 +401,17 @@ static bool rtc_probe(uint32_t *epoch_out) } /* At an RV3028 descriptor, a bit an RV3028 always reads as 0 marks - * some other part. Only two reads that agree rule out an RV3028. */ - if (d->cfg != NULL && !rv3028_time_bits_ok(blk)) { - if (i2c_burst_read(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) != 0) { - continue; - } - if (!rv3028_time_bits_ok(blk)) { - LOG_WRN("%s: not an RV3028, skipped", d->name); - continue; + * some other part. Only the same register showing one on two reads + * rules out an RV3028; a failed second read does not. */ + if (d->cfg != NULL && !rv3028_time_bits_ok(blk, blk)) { + uint8_t again[sizeof(blk)]; + + if (i2c_burst_read(d->bus, d->addr, d->time_reg, again, sizeof(again)) == 0) { + if (!rv3028_time_bits_ok(blk, again)) { + LOG_WRN("%s: not an RV3028, skipped", d->name); + continue; + } + memcpy(blk, again, sizeof(blk)); } } diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 7f9091c4..9a89016b 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -27,13 +27,15 @@ extern "C" { * 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 first chip that reads as an RTC (valid BCD - * time, or its power-loss flag set; never all 0xFF, nor, at a descriptor with - * rv3028-eeprom-config, with an RV3028 always-zero bit set on two reads) is - * remembered as the write-back target, valid time or not. Adopting one with + * time, or its power-loss flag set or unreadable; never all 0xFF, nor, at a + * descriptor with rv3028-eeprom-config, with an RV3028 always-zero bit set + * in the same register on two reads) is remembered as the write-back + * target, valid time or not. Adopting one with * rv3028-eeprom-config first waits out the chip's power-on refresh (up to * ~66 ms), then turns its backup switchover off while it reads the EEPROM - * and refreshes RAM from it (a few ms) and writes each byte that differs - * (~16 ms each). If that store fails, the config is set in RAM, which lasts + * and refreshes RAM from it, and writes each byte that differs (~16 ms + * each): about 14 ms on a RAK4631 when nothing is written, 52 ms for two + * bytes, plus up to 2 ms for switchover to take effect again (4.2.2). If that store fails, the config is set in RAM, which lasts * until the chip's next daily refresh, and the store is retried on the * system work queue every 10 minutes, at most 3 times. * Returns false if none present or no trustworthy time is held. From 26c5e3392e466e3604884bc8a56983ada96ace9c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 4 Oct 2026 08:36:12 -0700 Subject: [PATCH 18/26] rtc: take no time from a garbled RV3028 read; correct two contracts From the local pass over 334d9f7; the maintainer approved this third round of edits for the push: - When the confirming identity read failed, the first read, which had already shown an always-zero bit, was decoded anyway. A needless DSM switchover mid-read (manual 4.2.2) gives exactly that: an 0xFF tail, then a NACK. If the status read then succeeded with PORF clear, the invalid year failed the BCD check and the chip was not adopted for the boot, which is what 334d9f7 said it prevented; a rarer bit flip could seed a wrong time. Now a chip whose only reads show an always-zero bit is adopted as the write-back target but gives no time ("time unreadable"), and a second read that shows such a bit in another register counts as garbled too. - The binding said "on two reads" without "in the same register", which the code requires. - The header's "~16 ms" per byte did not reconcile with the measured 52 ms for two bytes; it now gives the measured figures only, and the switchover reaction for both modes: 2 ms in DSM (4.2.2), 15.625 ms in LSM (4.2.3). Checked on the RAK4631 + RAK12002: flash and reboot each restored the time with no warning. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 13 ++++++++++-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 20 +++++++++---------- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 19 +++++++++--------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 971082da..29f244f7 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -402,7 +402,10 @@ static bool rtc_probe(uint32_t *epoch_out) /* At an RV3028 descriptor, a bit an RV3028 always reads as 0 marks * some other part. Only the same register showing one on two reads - * rules out an RV3028; a failed second read does not. */ + * rules out an RV3028; a failed second read does not. Without a + * clean read the chip is adopted but no time is taken from it. */ + bool garbled = false; + if (d->cfg != NULL && !rv3028_time_bits_ok(blk, blk)) { uint8_t again[sizeof(blk)]; @@ -413,6 +416,7 @@ static bool rtc_probe(uint32_t *epoch_out) } memcpy(blk, again, sizeof(blk)); } + garbled = !rv3028_time_bits_ok(blk, blk); } /* Mask off flag/century bits. We trust this is a real RTC (vs. an @@ -432,7 +436,7 @@ static bool rtc_probe(uint32_t *epoch_out) BCD2BIN(db) >= 1 && BCD2BIN(ob) >= 1; bool unreliable = rtc_time_unreliable(d, blk); - if (!bcd_ok && !unreliable) { + if (!bcd_ok && !unreliable && !garbled) { continue; /* neither valid time nor a lost-power RTC => skip */ } @@ -441,6 +445,11 @@ static bool rtc_probe(uint32_t *epoch_out) rv3028_configure(d); } + if (garbled) { + LOG_WRN("%s present, time unreadable — clock will be set on the " + "next GPS/app/CLI sync", d->name); + continue; + } if (unreliable) { LOG_WRN("%s present, power-loss flag set — clock will be set " "on the next GPS/app/CLI sync", d->name); diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 9a89016b..d4066ff2 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -29,16 +29,16 @@ extern "C" { * *epoch_out and return true. The first chip that reads as an RTC (valid BCD * time, or its power-loss flag set or unreadable; never all 0xFF, nor, at a * descriptor with rv3028-eeprom-config, with an RV3028 always-zero bit set - * in the same register on two reads) is remembered as the write-back - * target, valid time or not. Adopting one with - * rv3028-eeprom-config first waits out the chip's power-on refresh (up to - * ~66 ms), then turns its backup switchover off while it reads the EEPROM - * and refreshes RAM from it, and writes each byte that differs (~16 ms - * each): about 14 ms on a RAK4631 when nothing is written, 52 ms for two - * bytes, plus up to 2 ms for switchover to take effect again (4.2.2). If that store fails, the config is set in RAM, which lasts - * until the chip's next daily refresh, and the store is retried on the - * system work queue every 10 minutes, at most 3 times. - * Returns false if none present or no trustworthy time is held. + * in the same register on two reads) is remembered as the write-back target, + * valid time or not. Adopting one with rv3028-eeprom-config first waits out + * the chip's power-on refresh (up to ~66 ms), then turns its backup + * switchover off while it reads the EEPROM and refreshes RAM from it, and + * writes each byte that differs. Measured on a RAK4631: about 14 ms when + * nothing is written, 52 ms for two bytes. Then switchover takes up to 2 ms + * to react in DSM, 15.6 ms in LSM (4.2.2-3). If that store fails, the config + * is set in RAM, which lasts until the chip's next daily refresh, and the + * store is retried on the system work queue every 10 minutes, at most 3 + * times. Returns false if none present or no trustworthy time is held. */ bool zephcore_rtc_restore(uint32_t *epoch_out); diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 7cd0c368..1b116d87 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -49,12 +49,13 @@ properties: EEPROM-backed configuration registers 0x35 (CLKOUT) to 0x37 (backup switchover and trickle charger), one triplet per register, masking implemented bits only (35h bits 5:4 are not); any other triplet is - ignored. A chip at the address whose always-zero time bits read 1 on - two reads is not adopted. When the chip is adopted, any register whose - masked bits differ is written to the chip's EEPROM, one byte at a time - with backup switchover held off, so the chip's own refresh from EEPROM - keeps it. If that fails, the config is set in the RAM mirror, which - lasts until the chip's next daily refresh, and the store is retried up - to 3 times, 10 minutes apart. Set it - only on a board whose backup supply is known: trickle charging suits - a supercapacitor or rechargeable cell, never a primary cell. + ignored. A chip at the address that shows an always-zero time bit in + the same register on two reads is not adopted. When the chip is + adopted, any register whose masked bits differ is written to the + chip's EEPROM, one byte at a time with backup switchover held off, + so the chip's own refresh from EEPROM keeps it. If that fails, the + config is set in the RAM mirror, which lasts until the chip's next + daily refresh, and the store is retried up to 3 times, 10 minutes + apart. Set it only on a board whose backup supply is known: trickle + charging suits a supercapacitor or rechargeable cell, never a + primary cell. From b4890469688e73b59b89d6d2006fc2f00be2a3b9 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 5 Oct 2026 07:20:00 -0700 Subject: [PATCH 19/26] rtc: bracket the RV3028 time write too; no store after a switchover Local review of the #98 rework: - A BSF set during the boot time read now also skips the EEPROM store for that boot: the EEPROM needs VDD (manual 4.6.8, p. 57), and a switchover has just shown it is not steady. - zephcore_rtc_save() at an rv3028-eeprom-config descriptor writes the time block with the same BSF bracket. A switchover mid-write keeps the bytes before the cut, a mixed time that reads as valid with PORF clear. The write is retried once, and if both fail the year register is zeroed, so the next boot reads "time not yet set" instead of restoring it. - The Kconfig help, header and binding now state the clean-identification gate, and that the RAM fallback lasts until the next refresh from EEPROM, which is daily only while EERD is clear. Co-Authored-By: Claude Opus 5.5 --- zephcore/Kconfig | 6 +-- zephcore/adapters/clock/ZephyrRTCDiscover.c | 37 ++++++++++++++----- zephcore/adapters/clock/ZephyrRTCDiscover.h | 20 ++++++---- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 25 +++++++------ 4 files changed, 57 insertions(+), 31 deletions(-) diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 8626a385..bd4874cd 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -903,9 +903,9 @@ config ZEPHCORE_RTC_AUTODISCOVER their own node; 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. A node with - rv3028-eeprom-config also has that configuration committed to the - chip's EEPROM when it differs, with backup switchover held off while - the EEPROM is accessed. Compact raw-I2C reader (no Zephyr RTC + rv3028-eeprom-config, once a read identifies the chip, also has that + configuration committed to the chip's EEPROM when it differs, with + backup switchover held off while the EEPROM is accessed. Compact raw-I2C reader (no Zephyr RTC subsystem). Safe to leave on: a board that declares no zephcore,rtc-i2c node compiles to nothing. diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index d36986df..a0aae39e 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -245,17 +245,19 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) #define RV3028_REG_CFG_LAST 0x37 /* EEPROM Backup */ #define RV3028_BACKUP_BSM 0x0C /* 37h switchover mode; 00 = disabled */ -/* Read the time block again, with BSF cleared before and checked after. A - * switchover to backup disables the chip's I2C (4.2), so the rest of a read - * comes back 0xFF; a tail starting inside the year byte can still be valid - * BCD, a year too high. False if the read is not known to be whole. */ -static bool rv3028_read_steady(const struct rtc_desc *d, uint8_t blk[7]) +/* Read or write the time block with BSF cleared before and checked after. A + * switchover to backup disables the chip's I2C mid-transfer (4.2): a read's + * tail comes back 0xFF, which inside the year byte can still be valid BCD, + * and a write keeps only the bytes before the cut. False unless the transfer + * is known to be whole. */ +static bool rv3028_steady(const struct rtc_desc *d, uint8_t blk[7], bool write) { uint8_t st; return i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_STATUS, RV3028_STATUS_BSF, 0) == 0 && - i2c_burst_read(d->bus, d->addr, d->time_reg, blk, 7) == 0 && + (write ? i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 7) + : i2c_burst_read(d->bus, d->addr, d->time_reg, blk, 7)) == 0 && i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) == 0 && !(st & RV3028_STATUS_BSF); } @@ -494,11 +496,13 @@ static bool rtc_probe(uint32_t *epoch_out) #if RTC_RV3028_CFG /* Only a clean identification is configured: the store writes to - * the device at once. */ + * the device at once. Nor right after a switchover: the EEPROM + * needs VDD (4.6.8). */ bool clean = v == RTC_FOUND; - if (clean && d->cfg != NULL && !rv3028_read_steady(d, blk)) { + if (clean && d->cfg != NULL && !rv3028_steady(d, blk, false)) { v = RTC_FOUND_GARBLED; + clean = false; } #endif if (s_active == NULL) { @@ -602,7 +606,22 @@ void zephcore_rtc_save(uint32_t epoch) 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) { + bool ok; + +#if RTC_RV3028_CFG + if (d->cfg != NULL) { + /* A torn write leaves a mixed time that reads as valid: write once + * more, then zero the year so the next boot does not restore it. */ + ok = rv3028_steady(d, blk, true) || rv3028_steady(d, blk, true); + if (!ok) { + (void)i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00); + } + } else +#endif + { + ok = i2c_burst_write(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) == 0; + } + if (!ok) { LOG_WRN("RTC %s: time write failed", d->name); return; } diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 13409cc8..a136526b 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -39,14 +39,15 @@ extern "C" { * At a descriptor with rv3028-eeprom-config, the time is read once more with * the chip's backup switch flag (BSF) cleared before and checked after, and * is not taken if the read failed or BSF was set. A chip adopted on a read - * that identified it (not on a failed second read) has that config stored: - * after the power-on refresh (up to ~66 ms), backup switchover is turned off - * while the EEPROM is read, each byte that differs is written, and a refresh - * from EEPROM restores it. Measured on a RAK4631: about 14 ms when nothing is - * written, 52 ms for two bytes, then up to 2 ms for DSM to react (4.2.2). If - * the store fails, the config is set in RAM, which lasts until the chip's - * next daily refresh, and the store is retried on the system work queue - * every 10 minutes, at most 3 times. Returns false if none present or no + * that identified it (not on a failed second read), and whose BSF read was + * clean, has that config stored: after the power-on refresh (up to ~66 ms), + * backup switchover is turned off while the EEPROM is read, each byte that + * differs is written, and a refresh from EEPROM restores it. Measured on a + * RAK4631: about 14 ms when nothing is written, 52 ms for two bytes, then up + * to 2 ms for DSM to react (4.2.2). If the store fails, the config is set in + * RAM, which lasts until the chip's next refresh from EEPROM (daily while + * EERD is clear), and the store is retried on the system work queue every + * 10 minutes, at most 3 times. Returns false if none present or no * trustworthy time is held. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -58,6 +59,9 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * If restore adopted nothing but skipped a candidate whose first read was all * 0xFF, the first save probes again, once per boot, in the caller's context. * A device ruled out on its second read is not probed again for that reason. + * At a descriptor with rv3028-eeprom-config the time is written with BSF + * cleared before and checked after, once more if that fails, and if both + * fail the year register is zeroed so the next boot takes no time from it. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index d4cc5aff..ae9c773d 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -56,15 +56,18 @@ properties: type: uint8-array description: > RV3028 only. (register, mask, value) triplets for the chip's - EEPROM-backed configuration registers 0x35 (CLKOUT) to 0x37 (backup - switchover and trickle charger), one triplet per register, masking - implemented bits only (35h bits 5:4 are not); any other triplet is - ignored. When a read identifies the chip and it is adopted, any + EEPROM-backed configuration registers 0x35 (CLKOUT) to 0x37 + (backup switchover and trickle charger), one triplet per register, + masking implemented bits only (35h bits 5:4 are not); any other + triplet is ignored. When a read identifies the chip, it is + adopted, and no switchover showed during its time read, any register whose masked bits differ is written to the chip's EEPROM, - one byte at a time with backup switchover held off, so the chip's own - refresh from EEPROM keeps it. If that fails, the config is set in the - RAM mirror, which lasts until the chip's next daily refresh, and the - store is retried up to 3 times, 10 minutes apart. The boot time read - is also checked against the chip's backup switch flag. Set it only on - a board whose backup supply is known: trickle charging suits a - supercapacitor or rechargeable cell, never a primary cell. + one byte at a time with backup switchover held off, so the chip's + own refresh from EEPROM keeps it. If that fails, the config is set + in the RAM mirror, which lasts until the chip's next refresh from + EEPROM (daily while EERD is clear), and the store is retried up to + 3 times, 10 minutes apart. The time is read at boot and written on + each sync with the chip's backup switch flag cleared before and + checked after. Set it only on a board whose backup supply is + known: trickle charging suits a supercapacitor or rechargeable + cell, never a primary cell. From 7a10074f4bab99eadcde7a7f2289f84aae330527 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 5 Oct 2026 07:39:21 -0700 Subject: [PATCH 20/26] rtc: repeat an unconfirmed RV3028 time write; retry a skipped store Second local review round, and the same findings from the MeshCore session's mirror of the previous commit: - The zero-year fallback is gone. Its write most likely NACKs inside the switchover it targets (4.2: I2C is off in VBACKUP and for tDEB after), and when the burst write never went out it could zero a good time. An unconfirmed write is now repeated every 5 s from a delayed work item, with the time run on from the save, until one is confirmed or a newer save replaces it. MeshCore's #3421 work takes the same position: an interrupted time write is never left standing silently. - A store skipped at boot (identification failed on the second read, or BSF during the time read) is now retried from the existing 10-minute retry, and each retry identifies the chip again before any write. Two reads that rule it out stop the retries. - The bracket's comment no longer claims a cut write keeps the bytes before the cut: 5.10 says only that integrity is not guaranteed. - Kconfig help states the switchover gate and the bracketed read and write; header and binding describe the retries. Co-Authored-By: Claude Opus 5.5 --- zephcore/Kconfig | 8 +- zephcore/adapters/clock/ZephyrRTCDiscover.c | 127 ++++++++++++------ zephcore/adapters/clock/ZephyrRTCDiscover.h | 26 ++-- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 8 +- 4 files changed, 110 insertions(+), 59 deletions(-) diff --git a/zephcore/Kconfig b/zephcore/Kconfig index bd4874cd..4423e2b0 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -903,9 +903,11 @@ config ZEPHCORE_RTC_AUTODISCOVER their own node; 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. A node with - rv3028-eeprom-config, once a read identifies the chip, also has that - configuration committed to the chip's EEPROM when it differs, with - backup switchover held off while the EEPROM is accessed. Compact raw-I2C reader (no Zephyr RTC + rv3028-eeprom-config, once a read identifies the chip with no backup + switchover during it, also has that configuration committed to the + chip's EEPROM when it differs, with backup switchover held off while + the EEPROM is accessed, and has its time read and written with the + chip's backup switch flag cleared before and checked after. Compact raw-I2C reader (no Zephyr RTC subsystem). Safe to leave on: a board that declares no zephcore,rtc-i2c node compiles to nothing. diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index a0aae39e..1e3edeee 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -227,6 +227,41 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) return RTC_FOUND; } +/* The 7-byte BCD time block for an epoch, in d's register order. */ +static void rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) +{ + 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 */ + + 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)); +} + +/* 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). */ +static void rtc_clear_power_flag(const struct rtc_desc *d) +{ + 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); + } + } +} + #if RTC_RV3028_CFG /* RV3028 registers for reading and writing its configuration EEPROM. */ @@ -248,8 +283,8 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) /* Read or write the time block with BSF cleared before and checked after. A * switchover to backup disables the chip's I2C mid-transfer (4.2): a read's * tail comes back 0xFF, which inside the year byte can still be valid BCD, - * and a write keeps only the bytes before the cut. False unless the transfer - * is known to be whole. */ + * and a write's data integrity is no longer guaranteed (5.10). False unless + * the transfer is known to be whole. */ static bool rv3028_steady(const struct rtc_desc *d, uint8_t blk[7], bool write) { uint8_t st; @@ -463,10 +498,45 @@ static void rv3028_configure(const struct rtc_desc *d) } } +/* Identify the chip again before each retry, since the store writes to it at + * once: configure on a clean read, stop if two reads rule it out, and try + * later on a failed read or a switchover. */ static void rv3028_cfg_retry_fn(struct k_work *work) { + const struct rtc_desc *d = s_active; + uint8_t blk[7]; + enum rtc_verdict v = rtc_identify(d, blk); + + ARG_UNUSED(work); + if (v == RTC_FOUND && rv3028_steady(d, blk, false)) { + rv3028_configure(d); + } else if (v != RTC_NOT_THIS && ++s_cfg_tries <= RTC_CFG_RETRIES) { + k_work_schedule(&s_cfg_retry, RTC_CFG_RETRY); + } +} + +/* A time write that a switchover may have cut is not left standing: it is + * repeated, with the time run on, until one is confirmed. */ +#define RTC_SAVE_RETRY K_SECONDS(5) + +static uint32_t s_save_epoch; +static int64_t s_save_at; +static void rv3028_save_retry_fn(struct k_work *work); +static K_WORK_DELAYABLE_DEFINE(s_save_retry, rv3028_save_retry_fn); + +static void rv3028_save_retry_fn(struct k_work *work) +{ + uint8_t blk[7]; + ARG_UNUSED(work); - rv3028_configure(s_active); + rtc_time_block(s_active, s_save_epoch + + (uint32_t)((k_uptime_get() - s_save_at) / 1000), blk); + if (!rv3028_steady(s_active, blk, true)) { + k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); + return; + } + rtc_clear_power_flag(s_active); + LOG_INF("RTC %s: time write confirmed on retry", s_active->name); } #endif /* RTC_RV3028_CFG */ @@ -510,6 +580,8 @@ static bool rtc_probe(uint32_t *epoch_out) #if RTC_RV3028_CFG if (clean) { rv3028_configure(d); + } else if (d->cfg != NULL) { + k_work_schedule(&s_cfg_retry, RTC_CFG_RETRY); } #endif } @@ -587,54 +659,27 @@ void zephcore_rtc_save(uint32_t epoch) } 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)); - - bool ok; + rtc_time_block(d, epoch, blk); #if RTC_RV3028_CFG if (d->cfg != NULL) { - /* A torn write leaves a mixed time that reads as valid: write once - * more, then zero the year so the next boot does not restore it. */ - ok = rv3028_steady(d, blk, true) || rv3028_steady(d, blk, true); - if (!ok) { - (void)i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00); + (void)k_work_cancel_delayable(&s_save_retry); + s_save_epoch = epoch; + s_save_at = k_uptime_get(); + if (!rv3028_steady(d, blk, true)) { + LOG_WRN("RTC %s: time write not confirmed, repeating every 5 s", + d->name); + k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); + return; } } else #endif - { - ok = i2c_burst_write(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) == 0; - } - if (!ok) { + 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); - } - } + rtc_clear_power_flag(d); LOG_DBG("RTC %s: persisted time", d->name); } diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index a136526b..d487cf2e 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -39,16 +39,17 @@ extern "C" { * At a descriptor with rv3028-eeprom-config, the time is read once more with * the chip's backup switch flag (BSF) cleared before and checked after, and * is not taken if the read failed or BSF was set. A chip adopted on a read - * that identified it (not on a failed second read), and whose BSF read was - * clean, has that config stored: after the power-on refresh (up to ~66 ms), - * backup switchover is turned off while the EEPROM is read, each byte that - * differs is written, and a refresh from EEPROM restores it. Measured on a - * RAK4631: about 14 ms when nothing is written, 52 ms for two bytes, then up - * to 2 ms for DSM to react (4.2.2). If the store fails, the config is set in - * RAM, which lasts until the chip's next refresh from EEPROM (daily while - * EERD is clear), and the store is retried on the system work queue every - * 10 minutes, at most 3 times. Returns false if none present or no - * trustworthy time is held. + * that identified it (not on a failed second read), with BSF clear, has that + * config stored at once: after the power-on refresh (up to ~66 ms), backup + * switchover is turned off while the EEPROM is read, each byte that differs + * is written, and a refresh from EEPROM restores it. Measured on a RAK4631: + * about 14 ms when nothing is written, 52 ms for two bytes, then up to 2 ms + * for DSM to react (4.2.2). If the store fails, the config is set in RAM, + * which lasts until the chip's next refresh from EEPROM (daily while EERD is + * clear). A failed store, or one skipped at boot, is retried on the system + * work queue every 10 minutes, at most 3 times, each retry identifying the + * chip again first. Returns false if none present or no trustworthy time is + * held. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -60,8 +61,9 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * 0xFF, the first save probes again, once per boot, in the caller's context. * A device ruled out on its second read is not probed again for that reason. * At a descriptor with rv3028-eeprom-config the time is written with BSF - * cleared before and checked after, once more if that fails, and if both - * fail the year register is zeroed so the next boot takes no time from it. + * cleared before and checked after. A write not confirmed that way is + * repeated every 5 s on the system work queue, with the time run on, until + * one is confirmed or a newer save replaces it. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index ae9c773d..b77cfaba 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -66,8 +66,10 @@ properties: own refresh from EEPROM keeps it. If that fails, the config is set in the RAM mirror, which lasts until the chip's next refresh from EEPROM (daily while EERD is clear), and the store is retried up to - 3 times, 10 minutes apart. The time is read at boot and written on - each sync with the chip's backup switch flag cleared before and - checked after. Set it only on a board whose backup supply is + 3 times, 10 minutes apart, identifying the chip again first; a + boot whose read could not identify it cleanly waits for those + retries. The time is read at boot and written on each sync with the + chip's backup switch flag cleared before and checked after, and a + write not confirmed is repeated until one is. Set it only on a board whose backup supply is known: trickle charging suits a supercapacitor or rechargeable cell, never a primary cell. From 3f51c58a80a607ffb2015771166975ac8ba7e578 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 5 Oct 2026 07:49:44 -0700 Subject: [PATCH 21/26] rtc: year-marker RV3028 time write; bound its repeat; shared EEbusy rule Third review round, authorized by Pieter, plus two rules agreed across the MeshCore, ZephCore and Zephyr RV3028 work: - Time write order: year 00h first, 00h-05h, then the real year, inside the BSF bracket. A write cut by a power loss, which takes the MCU down and so is never repeated, leaves year 2000: the next boot reads "time not yet set" and still adopts the chip, since identity ignores the year. Month 00h was rejected as the marker: #99's identity rules would then rule a real RV3028 out. Writing seconds restarts the prescaler (4.5.1), so no tick lands between the writes. MeshCore #3421's work uses the same order. - EEbusy: after a Refresh that did not complete, and for the RAM-only fallback, 37h is written with switchover on only once EEbusy reads 0; otherwise switchover stays off until a retry or the daily refresh (3.15.6). Same rule as Zephyr #121252's work and MeshCore #3545's. - A store skipped at boot counts as the first try, so it gets 3 retries like a failed one, not 4. - The time-write repeat stops after 12 attempts (about a minute) and logs once, so a locked or silent chip is not written every 5 s for the rest of uptime. - The header states that zephcore_rtc_save() must run on the system work queue, which its repeat shares state with. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 47 ++++++++++++++----- zephcore/adapters/clock/ZephyrRTCDiscover.h | 16 ++++--- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 6 ++- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 1e3edeee..d57f9794 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -280,6 +280,17 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) #define RV3028_REG_CFG_LAST 0x37 /* EEPROM Backup */ #define RV3028_BACKUP_BSM 0x0C /* 37h switchover mode; 00 = disabled */ +/* Year 00h first and the real year last: a write cut by a power loss, which + * no retry can repeat, leaves year 2000, read at boot as "time not yet set" + * rather than as a mixed time. Writing seconds restarts the prescaler + * (4.5.1), so no tick lands between the three writes. */ +static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) +{ + return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00) == 0 && + i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 6) == 0 && + i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, blk[6]) == 0; +} + /* Read or write the time block with BSF cleared before and checked after. A * switchover to backup disables the chip's I2C mid-transfer (4.2): a read's * tail comes back 0xFF, which inside the year byte can still be valid BCD, @@ -291,8 +302,8 @@ static bool rv3028_steady(const struct rtc_desc *d, uint8_t blk[7], bool write) return i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_STATUS, RV3028_STATUS_BSF, 0) == 0 && - (write ? i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 7) - : i2c_burst_read(d->bus, d->addr, d->time_reg, blk, 7)) == 0 && + (write ? rv3028_write_time(d, blk) + : i2c_burst_read(d->bus, d->addr, d->time_reg, blk, 7) == 0) && i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) == 0 && !(st & RV3028_STATUS_BSF); } @@ -426,10 +437,12 @@ static enum rv3028_store rv3028_store_config(const struct rtc_desc *d) } /* The Refresh did not run or did not finish, so RAM may still hold BSM - * 00. Once an EEPROM operation still running has had the chance to - * finish, write 37h as configured: RAM only, never an older value. */ - if (held && !refreshed) { - (void)rv3028_eeprom_idle(d); + * 00. Write 37h as configured, RAM only, once EEbusy reads 0. If it + * never does, leave switchover off rather than enable it during an + * EEPROM operation (3.15.6); the retry or the daily refresh restores + * it. Clearing EERD below while EEbusy may still be set is our + * judgement: 4.6.7 only describes it after EEbusy is 0. */ + if (held && !refreshed && rv3028_eeprom_idle(d)) { (void)i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, rv3028_cfg_backup_ram(d, backup)); } @@ -488,9 +501,10 @@ static void rv3028_configure(const struct rtc_desc *d) "(attempt %u)", d->name, s_cfg_tries); break; case RV3028_NOT_STORED: - (void)rv3028_eeprom_idle(d); + /* RAM only once EEbusy reads 0, as above. */ LOG_WRN("%s: config not stored in EEPROM, %s in RAM (attempt %u)", d->name, - rv3028_set_ram(d) ? "set" : "NOT set", s_cfg_tries); + rv3028_eeprom_idle(d) && rv3028_set_ram(d) ? "set" : "NOT set", + s_cfg_tries); break; } if (s_cfg_tries <= RTC_CFG_RETRIES) { @@ -515,12 +529,16 @@ static void rv3028_cfg_retry_fn(struct k_work *work) } } -/* A time write that a switchover may have cut is not left standing: it is - * repeated, with the time run on, until one is confirmed. */ +/* A time write that a switchover may have cut is repeated, with the time run + * on, until one is confirmed, for about a minute: a password-locked chip or + * one that stopped answering never confirms. A power loss that takes the MCU + * down too is covered by rv3028_write_time()'s order instead. */ #define RTC_SAVE_RETRY K_SECONDS(5) +#define RTC_SAVE_RETRIES 12 static uint32_t s_save_epoch; static int64_t s_save_at; +static uint8_t s_save_tries; static void rv3028_save_retry_fn(struct k_work *work); static K_WORK_DELAYABLE_DEFINE(s_save_retry, rv3028_save_retry_fn); @@ -532,7 +550,12 @@ static void rv3028_save_retry_fn(struct k_work *work) rtc_time_block(s_active, s_save_epoch + (uint32_t)((k_uptime_get() - s_save_at) / 1000), blk); if (!rv3028_steady(s_active, blk, true)) { - k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); + if (++s_save_tries < RTC_SAVE_RETRIES) { + k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); + } else { + LOG_WRN("RTC %s: time write not confirmed after %u tries", + s_active->name, s_save_tries + 1U); + } return; } rtc_clear_power_flag(s_active); @@ -581,6 +604,7 @@ static bool rtc_probe(uint32_t *epoch_out) if (clean) { rv3028_configure(d); } else if (d->cfg != NULL) { + s_cfg_tries++; /* the skipped store is the first try */ k_work_schedule(&s_cfg_retry, RTC_CFG_RETRY); } #endif @@ -667,6 +691,7 @@ void zephcore_rtc_save(uint32_t epoch) (void)k_work_cancel_delayable(&s_save_retry); s_save_epoch = epoch; s_save_at = k_uptime_get(); + s_save_tries = 0; if (!rv3028_steady(d, blk, true)) { LOG_WRN("RTC %s: time write not confirmed, repeating every 5 s", d->name); diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index d487cf2e..a411daae 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -46,9 +46,10 @@ extern "C" { * about 14 ms when nothing is written, 52 ms for two bytes, then up to 2 ms * for DSM to react (4.2.2). If the store fails, the config is set in RAM, * which lasts until the chip's next refresh from EEPROM (daily while EERD is - * clear). A failed store, or one skipped at boot, is retried on the system - * work queue every 10 minutes, at most 3 times, each retry identifying the - * chip again first. Returns false if none present or no trustworthy time is + * clear), and only once EEbusy reads 0; otherwise switchover is left off + * until a retry or that refresh. A failed store, or one skipped at boot, is + * retried on the system work queue every 10 minutes, at most 3 times, each + * retry identifying the chip again first. Returns false if none present or no trustworthy time is * held. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -61,9 +62,12 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * 0xFF, the first save probes again, once per boot, in the caller's context. * A device ruled out on its second read is not probed again for that reason. * At a descriptor with rv3028-eeprom-config the time is written with BSF - * cleared before and checked after. A write not confirmed that way is - * repeated every 5 s on the system work queue, with the time run on, until - * one is confirmed or a newer save replaces it. + * cleared before and checked after, year 00h first and the real year last, + * so a write cut by a power loss reads at boot as "time not yet set". A + * write not confirmed is repeated every 5 s on the system work queue, with + * the time run on, until one is confirmed, a newer save replaces it, or 12 + * attempts have failed. Call only from the system work queue: the repeat + * shares state with this call. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index b77cfaba..d05ed7be 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -69,7 +69,9 @@ properties: 3 times, 10 minutes apart, identifying the chip again first; a boot whose read could not identify it cleanly waits for those retries. The time is read at boot and written on each sync with the - chip's backup switch flag cleared before and checked after, and a - write not confirmed is repeated until one is. Set it only on a board whose backup supply is + chip's backup switch flag cleared before and checked after. A + write sets year 00h first and the real year last, so a write cut + by a power loss reads as "time not yet set", and a write not + confirmed is repeated every 5 s, up to 12 attempts. Set it only on a board whose backup supply is known: trickle charging suits a supercapacitor or rechargeable cell, never a primary cell. From b574fdf0cb5266d464e5705f5779f29339d04758 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 5 Oct 2026 08:01:02 -0700 Subject: [PATCH 22/26] rtc: run the RV3028 RAM fallback outside LOG_WRN The RAM fallback after a failed EEPROM store was evaluated inside LOG_WRN's arguments, and Zephyr does not evaluate them when logging is off (CONFIG_LOG=n in prj.conf, so every release image). A release build whose store failed therefore never set the config in RAM, and on a chip still holding its delivery EEPROM switchover stayed off. Only debug builds ran it. Present since 334d9f7. The fallback, and its EEbusy gate, now run into a bool before the log line; the release RTC object gains the RAM write it was missing. Also from the fourth local review pass: the binding states the EEbusy condition on the RAM fallback, the header counts the save repeat as 12 repeats (13 attempts, matching the log), and the write-order comment no longer claims no tick can land between all three writes. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 16 ++++++++----- zephcore/adapters/clock/ZephyrRTCDiscover.h | 2 +- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 24 ++++++++++--------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index d57f9794..bde7bf07 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -282,8 +282,9 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) /* Year 00h first and the real year last: a write cut by a power loss, which * no retry can repeat, leaves year 2000, read at boot as "time not yet set" - * rather than as a mixed time. Writing seconds restarts the prescaler - * (4.5.1), so no tick lands between the three writes. */ + * rather than as a mixed time. A tick before the burst is overwritten by + * it, and the burst's seconds restart the prescaler (4.5.1), so none lands + * before the real year unless the last write is a second late. */ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) { return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00) == 0 && @@ -500,13 +501,16 @@ static void rv3028_configure(const struct rtc_desc *d) LOG_WRN("%s: config stored, but EERD still set, so no daily refresh " "(attempt %u)", d->name, s_cfg_tries); break; - case RV3028_NOT_STORED: - /* RAM only once EEbusy reads 0, as above. */ + case RV3028_NOT_STORED: { + /* RAM only once EEbusy reads 0, as above. Not inside LOG_WRN: a + * build without logging does not evaluate its arguments. */ + bool set = rv3028_eeprom_idle(d) && rv3028_set_ram(d); + LOG_WRN("%s: config not stored in EEPROM, %s in RAM (attempt %u)", d->name, - rv3028_eeprom_idle(d) && rv3028_set_ram(d) ? "set" : "NOT set", - s_cfg_tries); + set ? "set" : "NOT set", s_cfg_tries); break; } + } if (s_cfg_tries <= RTC_CFG_RETRIES) { k_work_schedule(&s_cfg_retry, RTC_CFG_RETRY); } diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index a411daae..c29df7a8 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -66,7 +66,7 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * so a write cut by a power loss reads at boot as "time not yet set". A * write not confirmed is repeated every 5 s on the system work queue, with * the time run on, until one is confirmed, a newer save replaces it, or 12 - * attempts have failed. Call only from the system work queue: the repeat + * repeats (13 attempts in all) have failed. Call only from the system work queue: the repeat * shares state with this call. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index d05ed7be..2f6fed65 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -64,14 +64,16 @@ properties: register whose masked bits differ is written to the chip's EEPROM, one byte at a time with backup switchover held off, so the chip's own refresh from EEPROM keeps it. If that fails, the config is set - in the RAM mirror, which lasts until the chip's next refresh from - EEPROM (daily while EERD is clear), and the store is retried up to - 3 times, 10 minutes apart, identifying the chip again first; a - boot whose read could not identify it cleanly waits for those - retries. The time is read at boot and written on each sync with the - chip's backup switch flag cleared before and checked after. A - write sets year 00h first and the real year last, so a write cut - by a power loss reads as "time not yet set", and a write not - confirmed is repeated every 5 s, up to 12 attempts. Set it only on a board whose backup supply is - known: trickle charging suits a supercapacitor or rechargeable - cell, never a primary cell. + in the RAM mirror once the chip's EEbusy flag reads 0, and lasts + until the chip's next refresh from EEPROM (daily while EERD is + clear); if EEbusy never clears, switchover is left off until a + retry or that refresh. The store is retried up to 3 times, 10 + minutes apart, identifying the chip again first; a boot whose read + could not identify it cleanly waits for those retries. The time is + read at boot and written on each sync with the chip's backup + switch flag cleared before and checked after. A write sets year + 00h first and the real year last, so a write cut by a power loss + reads as "time not yet set", and a write not confirmed is repeated + every 5 s, up to 12 times. Set it only on a board whose backup + supply is known: trickle charging suits a supercapacitor or + rechargeable cell, never a primary cell. From 63e10d26edcc40480014a7c401bca8c3c6f49274 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 5 Oct 2026 08:09:51 -0700 Subject: [PATCH 23/26] rtc: correct four RV3028 contract claims; keep #99's header lines Comment and documentation only; the RTC object's code is byte-identical in release and debug builds. From the fourth local review pass: - A save is "confirmed" by acknowledgements and a clear BSF, not a read back, so a password-locked chip that ignores the writes passes; the comment no longer says it never confirms. - After a failed store, switchover is off only if the store had already disabled it; header and binding now say so. - The write-order comment credits the final year write, not the burst, with overwriting a tick that lands before it. - The Kconfig help names the "chip is the one adopted" condition. The header's RV3028 text is now its own paragraph after #99's, so #99's lines are no longer reflowed, and over-long lines are rewrapped. Co-Authored-By: Claude Opus 5.5 --- zephcore/Kconfig | 23 +++++------ zephcore/adapters/clock/ZephyrRTCDiscover.c | 14 ++++--- zephcore/adapters/clock/ZephyrRTCDiscover.h | 39 ++++++++++--------- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 23 +++++------ 4 files changed, 53 insertions(+), 46 deletions(-) diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 4423e2b0..31c7392a 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -899,17 +899,18 @@ config ZEPHCORE_RTC_AUTODISCOVER 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 or declaring - their own node; 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. A node with - rv3028-eeprom-config, once a read identifies the chip with no backup - switchover during it, also has that configuration committed to the - chip's EEPROM when it differs, with backup switchover held off while - the EEPROM is accessed, and has its time read and written with the - chip's backup switch flag cleared before and checked after. Compact raw-I2C reader (no Zephyr RTC - subsystem). Safe to leave on: a board that declares no zephcore,rtc-i2c - node compiles to nothing. + backed RTC opt in by including boards/common/rtc-i2c.dtsi or + declaring their own node; 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. A node with rv3028-eeprom-config, when its chip is the + one adopted and a read identifies it with no backup switchover + during it, also has that configuration committed to the chip's + EEPROM when it differs, with backup switchover held off while the + EEPROM is accessed, and has its time read and written with the + chip's backup switch flag cleared before and checked after. Compact + raw-I2C reader (no Zephyr RTC subsystem). Safe to leave on: a board + that declares no zephcore,rtc-i2c node compiles to nothing. menu "GPS Configuration" diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index bde7bf07..74a94f1d 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -282,9 +282,9 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) /* Year 00h first and the real year last: a write cut by a power loss, which * no retry can repeat, leaves year 2000, read at boot as "time not yet set" - * rather than as a mixed time. A tick before the burst is overwritten by - * it, and the burst's seconds restart the prescaler (4.5.1), so none lands - * before the real year unless the last write is a second late. */ + * rather than as a mixed time. The final year write overwrites whatever a + * tick before it changed, and the burst's seconds restart the prescaler + * (4.5.1), so no tick lands after it unless the last write is a second late. */ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) { return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00) == 0 && @@ -534,9 +534,11 @@ static void rv3028_cfg_retry_fn(struct k_work *work) } /* A time write that a switchover may have cut is repeated, with the time run - * on, until one is confirmed, for about a minute: a password-locked chip or - * one that stopped answering never confirms. A power loss that takes the MCU - * down too is covered by rv3028_write_time()'s order instead. */ + * on, until one is confirmed, for about a minute: a chip that stopped + * answering never confirms. Confirmed means acknowledged with BSF clear, not + * read back, so a password-locked chip that ignores the writes passes. A + * power loss that takes the MCU down too is covered by rv3028_write_time()'s + * order instead. */ #define RTC_SAVE_RETRY K_SECONDS(5) #define RTC_SAVE_RETRIES 12 diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index c29df7a8..c2d22aeb 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -35,22 +35,24 @@ extern "C" { * while the power-loss flag does not read as set (an unreadable flag counts * as not set), or, on the second read, all 0xFF. A failed second read never * rules a device out. A time is taken only from a clean read whose fields, - * hours and year are each in range (12-hour mode is not decoded). - * At a descriptor with rv3028-eeprom-config, the time is read once more with - * the chip's backup switch flag (BSF) cleared before and checked after, and - * is not taken if the read failed or BSF was set. A chip adopted on a read - * that identified it (not on a failed second read), with BSF clear, has that - * config stored at once: after the power-on refresh (up to ~66 ms), backup - * switchover is turned off while the EEPROM is read, each byte that differs - * is written, and a refresh from EEPROM restores it. Measured on a RAK4631: - * about 14 ms when nothing is written, 52 ms for two bytes, then up to 2 ms - * for DSM to react (4.2.2). If the store fails, the config is set in RAM, - * which lasts until the chip's next refresh from EEPROM (daily while EERD is - * clear), and only once EEbusy reads 0; otherwise switchover is left off - * until a retry or that refresh. A failed store, or one skipped at boot, is - * retried on the system work queue every 10 minutes, at most 3 times, each - * retry identifying the chip again first. Returns false if none present or no trustworthy time is - * held. + * hours and year are each in range (12-hour mode is not decoded). Returns + * false if none present or no trustworthy time is held. + * + * At a descriptor with rv3028-eeprom-config, the time is read once more + * with the chip's backup switch flag (BSF) cleared before and checked + * after, and is not taken if the read failed or BSF was set. A chip adopted + * on a read that identified it (not on a failed second read), with BSF + * clear, has that config stored at once: after the power-on refresh (up to + * ~66 ms), backup switchover is turned off while the EEPROM is read, each + * byte that differs is written, and a refresh from EEPROM restores it. + * Measured on a RAK4631: about 14 ms when nothing is written, 52 ms for two + * bytes, then up to 2 ms for DSM to react (4.2.2). If the store fails, the + * config is set in RAM, which lasts until the chip's next refresh from + * EEPROM (daily while EERD is clear), and only once EEbusy reads 0; + * otherwise switchover stays as the store left it, off once the store has + * disabled it, until a retry or that refresh. A failed store, or one + * skipped at boot, is retried on the system work queue every 10 minutes, at + * most 3 times, each retry identifying the chip again first. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -61,13 +63,14 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * If restore adopted nothing but skipped a candidate whose first read was all * 0xFF, the first save probes again, once per boot, in the caller's context. * A device ruled out on its second read is not probed again for that reason. + * * At a descriptor with rv3028-eeprom-config the time is written with BSF * cleared before and checked after, year 00h first and the real year last, * so a write cut by a power loss reads at boot as "time not yet set". A * write not confirmed is repeated every 5 s on the system work queue, with * the time run on, until one is confirmed, a newer save replaces it, or 12 - * repeats (13 attempts in all) have failed. Call only from the system work queue: the repeat - * shares state with this call. + * repeats (13 attempts in all) have failed. Call only from the system work + * queue: the repeat shares state with this call. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 2f6fed65..f241ecce 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -66,14 +66,15 @@ properties: own refresh from EEPROM keeps it. If that fails, the config is set in the RAM mirror once the chip's EEbusy flag reads 0, and lasts until the chip's next refresh from EEPROM (daily while EERD is - clear); if EEbusy never clears, switchover is left off until a - retry or that refresh. The store is retried up to 3 times, 10 - minutes apart, identifying the chip again first; a boot whose read - could not identify it cleanly waits for those retries. The time is - read at boot and written on each sync with the chip's backup - switch flag cleared before and checked after. A write sets year - 00h first and the real year last, so a write cut by a power loss - reads as "time not yet set", and a write not confirmed is repeated - every 5 s, up to 12 times. Set it only on a board whose backup - supply is known: trickle charging suits a supercapacitor or - rechargeable cell, never a primary cell. + clear); if EEbusy never clears, switchover stays as the store left + it, off once the store has disabled it, until a retry or that + refresh. The store is retried up to 3 times, 10 minutes apart, + identifying the chip again first; a boot whose read could not + identify it cleanly waits for those retries. The time is read at + boot and written on each sync with the chip's backup switch flag + cleared before and checked after. A write sets year 00h first and + the real year last, so a write cut by a power loss reads as "time + not yet set", and a write not confirmed is repeated every 5 s, up + to 12 times. Set it only on a board whose backup supply is known: + trickle charging suits a supercapacitor or rechargeable cell, + never a primary cell. From e0b888551402197bb4fe240b9c366d953e9d135b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 5 Oct 2026 08:14:08 -0700 Subject: [PATCH 24/26] rtc: cut RV3028 comments back to what the code does Comments only; the RTC object's code is byte-identical. The review of the previous commit found five claims that overstated the code, mostly about password-locked chips and tick timing. Rather than refine them, they are removed: the write-order, save-repeat, EEbusy and retry comments now state only what the code does, and the Kconfig help puts the adoption condition on the EEPROM commit alone, not on the BSF-bracketed time read and write. Co-Authored-By: Claude Opus 5.5 --- zephcore/Kconfig | 12 +++++------ zephcore/adapters/clock/ZephyrRTCDiscover.c | 23 +++++++-------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 31c7392a..189887ac 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -903,12 +903,12 @@ config ZEPHCORE_RTC_AUTODISCOVER declaring their own node; 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. A node with rv3028-eeprom-config, when its chip is the - one adopted and a read identifies it with no backup switchover - during it, also has that configuration committed to the chip's - EEPROM when it differs, with backup switchover held off while the - EEPROM is accessed, and has its time read and written with the - chip's backup switch flag cleared before and checked after. Compact + power-off. A node with rv3028-eeprom-config has its time read and + written with the chip's backup switch flag cleared before and + checked after, and, when its chip is the one adopted and a read + identifies it with no backup switchover during it, has that + configuration committed to the chip's EEPROM when it differs, with + backup switchover held off while the EEPROM is accessed. Compact raw-I2C reader (no Zephyr RTC subsystem). Safe to leave on: a board that declares no zephcore,rtc-i2c node compiles to nothing. diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 74a94f1d..fc3931b4 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -282,9 +282,7 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) /* Year 00h first and the real year last: a write cut by a power loss, which * no retry can repeat, leaves year 2000, read at boot as "time not yet set" - * rather than as a mixed time. The final year write overwrites whatever a - * tick before it changed, and the burst's seconds restart the prescaler - * (4.5.1), so no tick lands after it unless the last write is a second late. */ + * rather than as a mixed time. */ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) { return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00) == 0 && @@ -438,11 +436,10 @@ static enum rv3028_store rv3028_store_config(const struct rtc_desc *d) } /* The Refresh did not run or did not finish, so RAM may still hold BSM - * 00. Write 37h as configured, RAM only, once EEbusy reads 0. If it - * never does, leave switchover off rather than enable it during an - * EEPROM operation (3.15.6); the retry or the daily refresh restores - * it. Clearing EERD below while EEbusy may still be set is our - * judgement: 4.6.7 only describes it after EEbusy is 0. */ + * 00. Write 37h as configured, RAM only, once EEbusy reads 0, never + * during an EEPROM operation (3.15.6). Clearing EERD below while + * EEbusy may still be set is our judgement: 4.6.7 only describes it + * after EEbusy is 0. */ if (held && !refreshed && rv3028_eeprom_idle(d)) { (void)i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, rv3028_cfg_backup_ram(d, backup)); @@ -476,8 +473,7 @@ static bool rv3028_set_ram(const struct rtc_desc *d) } /* A failed store is retried on the system work queue, a few times per boot - * only: a password-locked chip, or a part at the address that is not an - * RV3028, never succeeds, and every attempt writes to it again. */ + * only, since every attempt writes to the chip again. */ #define RTC_CFG_RETRY K_MINUTES(10) #define RTC_CFG_RETRIES 3 @@ -534,11 +530,8 @@ static void rv3028_cfg_retry_fn(struct k_work *work) } /* A time write that a switchover may have cut is repeated, with the time run - * on, until one is confirmed, for about a minute: a chip that stopped - * answering never confirms. Confirmed means acknowledged with BSF clear, not - * read back, so a password-locked chip that ignores the writes passes. A - * power loss that takes the MCU down too is covered by rv3028_write_time()'s - * order instead. */ + * on, until one is confirmed, for about a minute. A power loss that takes the + * MCU down too is covered by rv3028_write_time()'s order instead. */ #define RTC_SAVE_RETRY K_SECONDS(5) #define RTC_SAVE_RETRIES 12 From a01e6ddbfcf9f12129c250b04224239c52599c45 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 5 Oct 2026 08:17:59 -0700 Subject: [PATCH 25/26] rtc: state the RV3028 cut-write window and config condition exactly Comments only; the RTC object's code is byte-identical. The year-last order covers a power loss after the first byte and before the last; a cut inside the final one-byte year write leaves that byte undefined (5.10). The Kconfig help separates the identifying read from the BSF-checked boot time read. Co-Authored-By: Claude Opus 5.5 --- zephcore/Kconfig | 13 +++++++------ zephcore/adapters/clock/ZephyrRTCDiscover.c | 10 +++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 189887ac..071d5436 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -905,12 +905,13 @@ config ZEPHCORE_RTC_AUTODISCOVER On every GPS/app/CLI sync the time is written back so it survives power-off. A node with rv3028-eeprom-config has its time read and written with the chip's backup switch flag cleared before and - checked after, and, when its chip is the one adopted and a read - identifies it with no backup switchover during it, has that - configuration committed to the chip's EEPROM when it differs, with - backup switchover held off while the EEPROM is accessed. Compact - raw-I2C reader (no Zephyr RTC subsystem). Safe to leave on: a board - that declares no zephcore,rtc-i2c node compiles to nothing. + checked after, and, when its chip is the one adopted, a read + identifies it, and the boot time read shows no backup switchover, + has that configuration committed to the chip's EEPROM when it + differs, with backup switchover held off while the EEPROM is + accessed. Compact raw-I2C reader (no Zephyr RTC subsystem). Safe to + leave on: a board that declares no zephcore,rtc-i2c node compiles + to nothing. menu "GPS Configuration" diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index fc3931b4..578e458d 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -280,9 +280,9 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) #define RV3028_REG_CFG_LAST 0x37 /* EEPROM Backup */ #define RV3028_BACKUP_BSM 0x0C /* 37h switchover mode; 00 = disabled */ -/* Year 00h first and the real year last: a write cut by a power loss, which - * no retry can repeat, leaves year 2000, read at boot as "time not yet set" - * rather than as a mixed time. */ +/* Year 00h first and the real year last: a power loss, which no retry can + * repeat, cutting the write after the first byte and before the last leaves + * year 2000, read at boot as "time not yet set" rather than as a mixed time. */ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) { return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00) == 0 && @@ -530,8 +530,8 @@ static void rv3028_cfg_retry_fn(struct k_work *work) } /* A time write that a switchover may have cut is repeated, with the time run - * on, until one is confirmed, for about a minute. A power loss that takes the - * MCU down too is covered by rv3028_write_time()'s order instead. */ + * on, until one is confirmed, for about a minute. For a power loss that takes + * the MCU down too, see rv3028_write_time()'s order. */ #define RTC_SAVE_RETRY K_SECONDS(5) #define RTC_SAVE_RETRIES 12 From 7c75633809c918dbe2a6d7ca0b4144068015fdde Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 5 Oct 2026 08:20:16 -0700 Subject: [PATCH 26/26] rtc: keep two RV3028 contract sentences to what always holds Comments only; the RTC object's code is byte-identical. A cut write's outcome at the next boot depends on where the cut lands (5.10), so the comment states only that no time is taken from it. The store can follow a clean read on a retry, not only at boot, and the Kconfig help says so. Co-Authored-By: Claude Opus 5.5 --- zephcore/Kconfig | 12 ++++++------ zephcore/adapters/clock/ZephyrRTCDiscover.c | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 071d5436..4f9cbf73 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -906,12 +906,12 @@ config ZEPHCORE_RTC_AUTODISCOVER power-off. A node with rv3028-eeprom-config has its time read and written with the chip's backup switch flag cleared before and checked after, and, when its chip is the one adopted, a read - identifies it, and the boot time read shows no backup switchover, - has that configuration committed to the chip's EEPROM when it - differs, with backup switchover held off while the EEPROM is - accessed. Compact raw-I2C reader (no Zephyr RTC subsystem). Safe to - leave on: a board that declares no zephcore,rtc-i2c node compiles - to nothing. + identifies it, and a checked time read just before the store shows + no backup switchover, has that configuration committed to the + chip's EEPROM when it differs, with backup switchover held off + while the EEPROM is accessed. Compact raw-I2C reader (no Zephyr RTC + subsystem). Safe to leave on: a board that declares no + zephcore,rtc-i2c node compiles to nothing. menu "GPS Configuration" diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 578e458d..59ecf13d 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -282,7 +282,7 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) /* Year 00h first and the real year last: a power loss, which no retry can * repeat, cutting the write after the first byte and before the last leaves - * year 2000, read at boot as "time not yet set" rather than as a mixed time. */ + * year 2000, so the next boot takes no time from it. */ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) { return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00) == 0 &&