From a1cd2536ed4d7a9555e7b6c041a9d18235d90480 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 08:51:53 -0700 Subject: [PATCH 01/14] rtc: mark an RV3028 time write with year A0h, then write it in one burst The RV3028 time write set year 00h, burst-wrote 00h-05h, then wrote the real year, so a write cut short left year 2000 and the next boot took no time from it. That split the write into three accesses against the manual's one-access rule (Application Manual Rev 1.4, 4.5), relying on the Seconds write restarting the prescaler, and used a real date as the "not set" mark. The write now sets 06h to A0h, which is not BCD, then writes all seven registers 00h-06h in one burst, year last. The chip keeps each byte as it is acknowledged, so a write cut short still leaves the mark. On read at an rv3028-eeprom-config descriptor, a year of A0h-FEh reads as "time not yet set": a New Year counts a left mark on (A0h to A1h, A9h to B0h) and never into BCD. FFh stays a failed read, since a read cut short returns 1s and the year is the last byte. The BSF bracket and the 5 s retry around the write are unchanged, and so is the 2025 floor, so a power-on-reset chip reading 2000-01-01 still gives no time. The same write order and year handling are used by Zephyr and MeshCore for the RV3028; the three are kept consistent in behaviour. Link: https://github.com/zephyrproject-rtos/zephyr/pull/121389 Link: https://github.com/meshcore-dev/MeshCore/pull/3421 Link: https://github.com/liquidraver/ZephCore/pull/98 Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 21 +++++++++++++++------ zephcore/adapters/clock/ZephyrRTCDiscover.h | 9 ++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 7f0b6b56..a591480f 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -281,15 +281,15 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) #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 RV3028_YEAR_UNSET 0xA0 /* not BCD; a year of A0h-FEh reads "not set" */ -/* 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, so the next boot takes no time from it. */ +/* Mark the year unset, then write all seven registers in one access (4.5), + * year last: the chip keeps each byte as it is acknowledged, so a write cut + * short leaves the mark and 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 && - 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; + return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0 && + i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 7) == 0; } /* Read or write the time block with BSF cleared before and checked after. A @@ -628,6 +628,15 @@ static bool rtc_probe(uint32_t *epoch_out) uint8_t sb = blk[0] & 0x7F, mb = blk[1] & 0x7F, hb = blk[2] & 0x3F; uint8_t db = blk[d->date_index] & 0x3F, ob = blk[5] & 0x1F, yb = blk[6]; +#if RTC_RV3028_CFG + /* A New Year counts a left mark on (A0h to A1h, A9h to B0h), never into + * BCD. FFh is a read cut short. */ + if (d->cfg != NULL && yb >= RV3028_YEAR_UNSET && yb != 0xFF) { + LOG_INF("%s present, time not yet set (year %02Xh)", d->name, yb); + continue; + } +#endif + /* Identity leaves the hours and year unchecked, and may have let the * other fields pass on a flag read since; a time needs them all. */ if (!rtc_fields_ok(d, blk) || !bcd_field_ok(hb, 23) || !bcd_field_ok(yb, 99)) { diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index c2d22aeb..02d876ca 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -40,7 +40,9 @@ 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 + * after, and is not taken if the read failed or BSF was set. A year of + * A0h-FEh there, the mark a cut write leaves, reads as "time not yet set"; + * FFh is a failed read. 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 @@ -65,8 +67,9 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * 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 + * cleared before and checked after: year A0h first, then all seven + * registers in one burst, year last, so a write cut short 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 From 2f5c639baaa0f1b046b65ce9b4ba4317cb4f122c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:00:39 -0700 Subject: [PATCH 02/14] dts: describe the RV3028 A0h year mark in the rtc-i2c binding The binding still described the old write order (year 00h first, the real year last). Co-Authored-By: Claude Opus 5.5 --- zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index efde7677..688beaee 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -82,9 +82,9 @@ properties: 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: + cleared before and checked after. A write sets the year to A0h, + then all seven registers in one burst, year last, so a write cut + short 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 ed8802f558773f28716374f477e45154f98d732a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:04:25 -0700 Subject: [PATCH 03/14] rtc: say a write cut after the A0h mark leaves it, not any cut write Local review: a write cut on the A0h byte itself leaves the old year and the old, intact time, so the docs now say "cut after the mark". The read comment no longer says a counted-on mark never reaches BCD, since only A0h, A9h and FFh were rolled over. The header lines the first edit left as fragments are rewrapped, and so is the binding text. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 10 ++++----- zephcore/adapters/clock/ZephyrRTCDiscover.h | 22 +++++++++---------- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 8 +++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index a591480f..21ad45d8 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -283,9 +283,9 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) #define RV3028_BACKUP_BSM 0x0C /* 37h switchover mode; 00 = disabled */ #define RV3028_YEAR_UNSET 0xA0 /* not BCD; a year of A0h-FEh reads "not set" */ -/* Mark the year unset, then write all seven registers in one access (4.5), - * year last: the chip keeps each byte as it is acknowledged, so a write cut - * short leaves the mark and the next boot takes no time from it. */ +/* Mark the year A0h, then write all seven registers in one access (4.5), + * year last. The chip keeps each byte as it is acknowledged, so a write cut + * after the mark leaves it, and 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, RV3028_YEAR_UNSET) == 0 && @@ -629,8 +629,8 @@ static bool rtc_probe(uint32_t *epoch_out) uint8_t db = blk[d->date_index] & 0x3F, ob = blk[5] & 0x1F, yb = blk[6]; #if RTC_RV3028_CFG - /* A New Year counts a left mark on (A0h to A1h, A9h to B0h), never into - * BCD. FFh is a read cut short. */ + /* A New Year counts a left mark on (A0h to A1h, A9h to B0h). FFh is a + * read cut short. */ if (d->cfg != NULL && yb >= RV3028_YEAR_UNSET && yb != 0xFF) { LOG_INF("%s present, time not yet set (year %02Xh)", d->name, yb); continue; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 02d876ca..d933d0ed 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -40,9 +40,7 @@ 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 year of - * A0h-FEh there, the mark a cut write leaves, reads as "time not yet set"; - * FFh is a failed read. A chip adopted + * 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 @@ -54,7 +52,10 @@ extern "C" { * 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. + * most 3 times, each retry identifying the chip again first. At such a + * descriptor a year of A0h-FEh is the mark a cut time write leaves, as + * written or counted on by a New Year, and reads as "time not yet set"; FFh + * is a failed read. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -67,13 +68,12 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * 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 A0h first, then all seven - * registers in one burst, year last, so a write cut short 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. + * cleared before and checked after: the year marked A0h, then all seven + * registers in one burst, so a write cut after the mark 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. */ 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 688beaee..1fdae05e 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -84,7 +84,7 @@ properties: boot and written on each sync with the chip's backup switch flag cleared before and checked after. A write sets the year to A0h, then all seven registers in one burst, year last, so a write cut - short 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. + after the mark 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 b9fb1246a2cac36fa6033f4f106bd9a370852dfe Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:08:55 -0700 Subject: [PATCH 04/14] rtc: say the A0h-FEh range holds the mark, not that every value is one Local review: a counted-on mark skips xAh-xFh, and another tool can write any of them, so the header no longer calls the whole range the mark. The behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index d933d0ed..561fb916 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -53,9 +53,9 @@ extern "C" { * 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. At such a - * descriptor a year of A0h-FEh is the mark a cut time write leaves, as - * written or counted on by a New Year, and reads as "time not yet set"; FFh - * is a failed read. + * descriptor a year of A0h-FEh reads as "time not yet set": that range holds + * the A0h mark a cut time write leaves and what a New Year counts it on to. + * FFh is a failed read. */ bool zephcore_rtc_restore(uint32_t *epoch_out); From e3f48f8af627c1e07b959a49223e4e99fbed1771 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:21:00 -0700 Subject: [PATCH 05/14] rtc: refuse to write a year outside 2000-2099 The year register holds two BCD digits and no century, and the save encoded y % 100. So a time before 2000 was stored as a year near the end of the century: a CLI `time 900000000` (1998), accepted by a node whose clock had no time yet, was written as year 98 and restored at the next boot as 2098, past the 2025 floor. A year from 2100 was stored as 2100-2106 minus a century, which the floor did reject. On the RV3028, BCD-encoding 2100 as a three-digit year would also equal the A0h mark, which is why Zephyr and MeshCore bound the write to 2000-2099 too. rtc_time_block() now fails outside 2000-2099, and the save writes nothing then, for every chip. At an rv3028-eeprom-config descriptor it writes the A0h mark instead and drops any pending retry, so the next boot takes no time rather than the older time the chip still holds. A retry whose run-on time crosses 2099 does the same. Other chips keep their older time. Local review: the docs now say a write cut between the mark and the year byte gives no time, rather than any cut after the mark, and that a mark gives no time rather than always logging "time not yet set", since a set power-loss flag or a switchover logs first. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 41 ++++++++++++++++--- zephcore/adapters/clock/ZephyrRTCDiscover.h | 21 ++++++---- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 10 +++-- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 21ad45d8..4a4967ad 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -229,8 +229,10 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) return RTC_FOUND; } -/* The 7-byte 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]) +/* The 7-byte time block for an epoch, in d's register order. False outside + * 2000-2099: the year register has two BCD digits and no century, so any + * other year would be stored as one inside that range. */ +static bool rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) { int y; unsigned m, day; @@ -241,6 +243,9 @@ static void rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk unsigned sec = rem % 60; unsigned dow = (unsigned)(((epoch / 86400) + 4) % 7); /* 1970-01-01 = Thu */ + if (y < 2000 || y > 2099) { + return false; + } blk[0] = BIN2BCD(sec); blk[1] = BIN2BCD(min); blk[2] = BIN2BCD(hour); @@ -249,6 +254,7 @@ static void rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk blk[d->date_index == 4 ? 3 : 4] = d->week_one_hot ? (uint8_t)BIT(dow) : (uint8_t)dow; blk[5] = BIN2BCD(m); blk[6] = BIN2BCD((unsigned)(y % 100)); + return true; } /* Clear the power-loss flag (for chips whose flag is a separate reg; @@ -285,13 +291,23 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) /* Mark the year A0h, then write all seven registers in one access (4.5), * year last. The chip keeps each byte as it is acknowledged, so a write cut - * after the mark leaves it, and the next boot takes no time from it. */ + * between the mark and the year byte leaves the mark, and 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, RV3028_YEAR_UNSET) == 0 && i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 7) == 0; } +/* A time outside 2000-2099 is not written. The year is marked instead, so the + * next boot takes no time rather than the older one the chip still holds. */ +static void rv3028_refuse_time(const struct rtc_desc *d) +{ + (void)i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET); + LOG_WRN("RTC %s: time outside 2000-2099 not written, year marked unset", + d->name); +} + /* 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, @@ -548,8 +564,11 @@ static void rv3028_save_retry_fn(struct k_work *work) uint8_t blk[7]; ARG_UNUSED(work); - rtc_time_block(s_active, s_save_epoch + - (uint32_t)((k_uptime_get() - s_save_at) / 1000), blk); + if (!rtc_time_block(s_active, s_save_epoch + + (uint32_t)((k_uptime_get() - s_save_at) / 1000), blk)) { + rv3028_refuse_time(s_active); + return; + } if (!rv3028_steady(s_active, blk, true)) { if (++s_save_tries < RTC_SAVE_RETRIES) { k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); @@ -695,7 +714,17 @@ void zephcore_rtc_save(uint32_t epoch) const struct rtc_desc *d = s_active; uint8_t blk[7]; - rtc_time_block(d, epoch, blk); + if (!rtc_time_block(d, epoch, blk)) { +#if RTC_RV3028_CFG + if (d->cfg != NULL) { + (void)k_work_cancel_delayable(&s_save_retry); + rv3028_refuse_time(d); + return; + } +#endif + LOG_WRN("RTC %s: time outside 2000-2099 not written", d->name); + return; + } #if RTC_RV3028_CFG if (d->cfg != NULL) { (void)k_work_cancel_delayable(&s_save_retry); diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 561fb916..5bcce38f 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -53,9 +53,8 @@ extern "C" { * 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. At such a - * descriptor a year of A0h-FEh reads as "time not yet set": that range holds - * the A0h mark a cut time write leaves and what a New Year counts it on to. - * FFh is a failed read. + * descriptor no time is taken from a year of A0h-FEh, the range that holds + * the A0h mark and what a New Year counts it on to. FFh is a failed read. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -69,11 +68,17 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * * At a descriptor with rv3028-eeprom-config the time is written with BSF * cleared before and checked after: the year marked A0h, then all seven - * registers in one burst, so a write cut after the mark 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. + * registers in one burst, so a write cut between the mark and the year byte + * gives no time at the next boot. 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. + * + * A time outside 2000-2099 is not written, since the chip's two-digit year + * would store it as one inside that range. The chip keeps its older time, + * except at an rv3028-eeprom-config descriptor, where its year is marked + * A0h so that the next boot takes no time. */ 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 1fdae05e..72f17b11 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -84,7 +84,9 @@ properties: boot and written on each sync with the chip's backup switch flag cleared before and checked after. A write sets the year to A0h, then all seven registers in one burst, year last, so a write cut - after the mark 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. + between the mark and the year byte gives no time at the next boot, + and a write not confirmed is repeated every 5 s, up to 12 times. A + time outside 2000-2099 is not written; the year is marked A0h + instead. Set it only on a board whose backup supply is known: + trickle charging suits a supercapacitor or rechargeable cell, + never a primary cell. From b0abaff0772f8d9b7a5a8a6818076c79f5105b27 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:28:29 -0700 Subject: [PATCH 06/14] rtc: confirm and repeat the RV3028 year mark like a time write Local review of the range check: the refusal ignored whether its A0h write landed, logged "year marked unset" either way and had already dropped any pending retry, so a failed mark left the chip's older time for the next boot to restore. The mark is now checked and, if it fails, repeated every 5 s up to the same 13 attempts, sharing the time write's repeat state; a newer save replaces it either way. The header says only a time within 2000-2099 is saved, and the 2000-2099 reason is ZephCore's two-digit year, since the DS3231 and PCF8563 have a century bit that ZephCore does not use. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 59 +++++++++++---------- zephcore/adapters/clock/ZephyrRTCDiscover.h | 17 +++--- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 4a4967ad..3487b137 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -230,7 +230,7 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) } /* The 7-byte time block for an epoch, in d's register order. False outside - * 2000-2099: the year register has two BCD digits and no century, so any + * 2000-2099: the year is kept as two BCD digits with no century, so any * other year would be stored as one inside that range. */ static bool rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) { @@ -299,13 +299,11 @@ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 7) == 0; } -/* A time outside 2000-2099 is not written. The year is marked instead, so the - * next boot takes no time rather than the older one the chip still holds. */ -static void rv3028_refuse_time(const struct rtc_desc *d) +/* For a time outside 2000-2099, which is not written: mark the year alone, + * so the next boot takes no time rather than the older one the chip holds. */ +static bool rv3028_mark_year(const struct rtc_desc *d) { - (void)i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET); - LOG_WRN("RTC %s: time outside 2000-2099 not written, year marked unset", - d->name); + return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0; } /* Read or write the time block with BSF cleared before and checked after. A @@ -556,6 +554,7 @@ static void rv3028_cfg_retry_fn(struct k_work *work) static uint32_t s_save_epoch; static int64_t s_save_at; static uint8_t s_save_tries; +static bool s_save_mark; /* the repeat writes the year mark, not a time */ static void rv3028_save_retry_fn(struct k_work *work); static K_WORK_DELAYABLE_DEFINE(s_save_retry, rv3028_save_retry_fn); @@ -564,20 +563,25 @@ static void rv3028_save_retry_fn(struct k_work *work) uint8_t blk[7]; ARG_UNUSED(work); - if (!rtc_time_block(s_active, s_save_epoch + + if (!s_save_mark && + !rtc_time_block(s_active, s_save_epoch + (uint32_t)((k_uptime_get() - s_save_at) / 1000), blk)) { - rv3028_refuse_time(s_active); - return; + s_save_mark = true; /* the run-on time left 2000-2099 */ } - if (!rv3028_steady(s_active, blk, true)) { + if (s_save_mark ? !rv3028_mark_year(s_active) : !rv3028_steady(s_active, blk, true)) { 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); + LOG_WRN("RTC %s: %s not confirmed after %u tries", s_active->name, + s_save_mark ? "year mark" : "time write", s_save_tries + 1U); } return; } + if (s_save_mark) { + LOG_WRN("RTC %s: time outside 2000-2099 not written, year marked unset", + s_active->name); + return; + } rtc_clear_power_flag(s_active); LOG_INF("RTC %s: time write confirmed on retry", s_active->name); } @@ -714,32 +718,31 @@ void zephcore_rtc_save(uint32_t epoch) const struct rtc_desc *d = s_active; uint8_t blk[7]; - if (!rtc_time_block(d, epoch, blk)) { -#if RTC_RV3028_CFG - if (d->cfg != NULL) { - (void)k_work_cancel_delayable(&s_save_retry); - rv3028_refuse_time(d); - return; - } -#endif - LOG_WRN("RTC %s: time outside 2000-2099 not written", d->name); - return; - } + bool in_range = rtc_time_block(d, epoch, blk); #if RTC_RV3028_CFG if (d->cfg != NULL) { (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); + s_save_mark = !in_range; + if (s_save_mark ? !rv3028_mark_year(d) : !rv3028_steady(d, blk, true)) { + LOG_WRN("RTC %s: %s not confirmed, repeating every 5 s", d->name, + s_save_mark ? "year mark" : "time write"); k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); return; } + if (s_save_mark) { + LOG_WRN("RTC %s: time outside 2000-2099 not written, year marked " + "unset", d->name); + return; + } } else #endif - if (i2c_burst_write(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) != 0) { + if (!in_range) { + LOG_WRN("RTC %s: time outside 2000-2099 not written", d->name); + return; + } else 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; } diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 5bcce38f..ca3b5267 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -10,8 +10,8 @@ * * 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 - * ZephyrRTCClock::setCurrentTime() writes it back via zephcore_rtc_save(), so - * time survives the next power-off. + * ZephyrRTCClock::setCurrentTime() within 2000-2099 writes it back via + * zephcore_rtc_save(), so time survives the next power-off. */ #pragma once @@ -59,8 +59,8 @@ extern "C" { 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 + * Persist an authoritative epoch within 2000-2099 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 a candidate whose first read was all * 0xFF, the first save probes again, once per boot, in the caller's context. @@ -75,10 +75,11 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * have failed. Call only from the system work queue: the repeat shares * state with this call. * - * A time outside 2000-2099 is not written, since the chip's two-digit year - * would store it as one inside that range. The chip keeps its older time, - * except at an rv3028-eeprom-config descriptor, where its year is marked - * A0h so that the next boot takes no time. + * A time outside 2000-2099 is not written, since the year is kept as two + * BCD digits with no century, and the chip keeps its older time. At an + * rv3028-eeprom-config descriptor the year is marked A0h instead, so the + * next boot takes no time; the mark is confirmed and repeated like a time + * write, and a repeat whose run-on time leaves 2000-2099 marks instead. */ void zephcore_rtc_save(uint32_t epoch); From fdf2332477b7d4be16b9d0eb5d132dd140d5f3cd Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:40:54 -0700 Subject: [PATCH 07/14] rtc: bracket the RV3028 year mark with BSF like a time write Local review: the mark was confirmed by its acknowledgement alone, without the BSF clear-before / check-after a time write gets, so a switchover during it (5.10) could leave the byte unstored while the log said "year marked unset". It now takes the same bracket, and a failure is repeated. A repeat that switches from writing the time to marking restarts the try count, so the final warning counts mark attempts only. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 12 ++++++++++-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 5 +++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 3487b137..cb5c93a7 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -300,10 +300,17 @@ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) } /* For a time outside 2000-2099, which is not written: mark the year alone, - * so the next boot takes no time rather than the older one the chip holds. */ + * so the next boot takes no time rather than the older one the chip holds. + * BSF is cleared before and checked after, as for a time write. */ static bool rv3028_mark_year(const struct rtc_desc *d) { - return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0; + uint8_t st; + + return i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_STATUS, + RV3028_STATUS_BSF, 0) == 0 && + i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0 && + i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) == 0 && + !(st & RV3028_STATUS_BSF); } /* Read or write the time block with BSF cleared before and checked after. A @@ -567,6 +574,7 @@ 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)) { s_save_mark = true; /* the run-on time left 2000-2099 */ + s_save_tries = 0; } if (s_save_mark ? !rv3028_mark_year(s_active) : !rv3028_steady(s_active, blk, true)) { if (++s_save_tries < RTC_SAVE_RETRIES) { diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index ca3b5267..c64495e0 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -60,8 +60,9 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); /* * Persist an authoritative epoch within 2000-2099 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. + * 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 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. From b4aebca58c3795b8db09f1bd727fdf3b928ddc47 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:51:04 -0700 Subject: [PATCH 08/14] rtc: keep one 13-attempt cap when a repeat switches to marking Local review of fdf2332: restarting the try count when a repeat switched from writing the time to marking let one save repeat up to 23 times, against the documented 13 attempts, and its final log overstated the mark attempts by one. The count now runs across both, and the final log says "save not confirmed" rather than naming one kind. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index cb5c93a7..0943f058 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -574,14 +574,13 @@ 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)) { s_save_mark = true; /* the run-on time left 2000-2099 */ - s_save_tries = 0; } if (s_save_mark ? !rv3028_mark_year(s_active) : !rv3028_steady(s_active, blk, true)) { if (++s_save_tries < RTC_SAVE_RETRIES) { k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); } else { - LOG_WRN("RTC %s: %s not confirmed after %u tries", s_active->name, - s_save_mark ? "year mark" : "time write", s_save_tries + 1U); + LOG_WRN("RTC %s: save not confirmed after %u tries", + s_active->name, s_save_tries + 1U); } return; } From e53d3a04ed9aea2fcc43f547af5b39787822802a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:57:57 -0700 Subject: [PATCH 09/14] rtc: say the 2000-2099 limit is ZephCore's year encoding Local review: the DS3231 and PCF8563 have a century bit, which ZephCore writes as 0 and masks off on read, so the limit is the encoding rather than every chip. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 4 ++-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 0943f058..96926424 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -230,8 +230,8 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) } /* The 7-byte time block for an epoch, in d's register order. False outside - * 2000-2099: the year is kept as two BCD digits with no century, so any - * other year would be stored as one inside that range. */ + * 2000-2099: ZephCore writes the year as two BCD digits and no century bit, + * so any other year would be stored as one inside that range. */ static bool rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) { int y; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index c64495e0..353e6a4e 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -76,11 +76,11 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * have failed. Call only from the system work queue: the repeat shares * state with this call. * - * A time outside 2000-2099 is not written, since the year is kept as two - * BCD digits with no century, and the chip keeps its older time. At an - * rv3028-eeprom-config descriptor the year is marked A0h instead, so the - * next boot takes no time; the mark is confirmed and repeated like a time - * write, and a repeat whose run-on time leaves 2000-2099 marks instead. + * A time outside 2000-2099 is not written, since ZephCore writes the year + * as two BCD digits and no century bit, and the chip keeps its older time. + * At an rv3028-eeprom-config descriptor the year is marked A0h instead, so + * the next boot takes no time; the mark is confirmed and repeated like a + * time write, and a repeat whose run-on time leaves 2000-2099 marks instead. */ void zephcore_rtc_save(uint32_t epoch); From 3bfdaa87256246b0bec1f3978cb701dee3842f53 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 14:15:56 -0700 Subject: [PATCH 10/14] rtc: restore two comment corrections the merge trim lost Local review: the trimmed rv3028_write_time() comment said a write cut before the year byte leaves the mark, which a cut on the A0h byte itself does not (ed8802f had this right), and the trimmed header called the whole A0h-FEh range the mark a cut write leaves (b9fb124 had narrowed it). The time block comment again names the two-digit year as ZephCore's encoding. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 4 ++-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index b8144647..d56c9d85 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -225,7 +225,7 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) } /* The 7-byte time block for an epoch, in d's register order. False outside - * 2000-2099, which a two-digit BCD year with no century bit cannot hold. */ + * 2000-2099: ZephCore writes the year as two BCD digits and no century bit. */ static bool rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) { int y; @@ -284,7 +284,7 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) #define RV3028_YEAR_UNSET 0xA0 /* not BCD; a year of A0h-FEh reads "not set" */ /* Mark the year A0h, then write all seven registers in one access (4.5): - * a write cut before the year byte leaves the mark. */ + * a write cut between the mark and the year byte leaves the mark. */ 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, RV3028_YEAR_UNSET) == 0 && diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 323df3e0..76ea11f3 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -32,7 +32,7 @@ extern "C" { * * A descriptor with rv3028-eeprom-config also has that config stored in the * chip's EEPROM, retried on the system work queue if it fails. There, a year - * of A0h-FEh, the mark a cut time write leaves, gives no time. + * of A0h-FEh gives no time: the A0h mark, or a New Year's count on from it. */ bool zephcore_rtc_restore(uint32_t *epoch_out); From 9550a815b5c62cb48abfeb726060cd06db480f3b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 14:21:47 -0700 Subject: [PATCH 11/14] rtc: state the A0h-FEh rule without a reason, and the 2000-2099 write range Local review: "the A0h mark, or a New Year's count on from it" is false for the xAh-xFh values in the range, which a count from A0h never reaches; the code and header now state the rule alone. The Kconfig help and ARCHITECTURE.md said every sync writes the RTC, which a time outside 2000-2099 no longer does. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/Kconfig | 4 ++-- zephcore/adapters/clock/ZephyrRTCDiscover.c | 2 +- zephcore/adapters/clock/ZephyrRTCDiscover.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 43430a80..e07f786b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -860,7 +860,7 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC **Telemetry** (`REQ_TYPE_GET_TELEMETRY_DATA`, every role, upstream's shape): battery on channel 1, then `sensors.querySensors()` (`adapters/sensors/ZephyrSensorManager.cpp`): GPS position on channel 1 while the GPS is on and has a fix, each environment sensor and power-monitor channel found at boot on its own channel from 2 up (probe order), board-local analog sensors (T1000-E) on channel 1; then the MCU temperature on channel 1. Servers give guests battery + MCU temperature only and honour the requester's inverse permission mask, as upstream. Encoded by `helpers/compat/CayenneLPP.h`: upstream's library API and wire format, but values round to the nearest step where the library truncates. -**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one (coalesced on the system work queue), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. A chip is adopted at boot only once it is identified (`adapters/clock/ZephyrRTCDiscover.c`), because other parts can answer at the same address: a device is passed over when two reads each show a bit set that the chip's data sheet fixes at zero, or a time field out of range, and a time is restored only from a read in which every field is valid. The time is written in each chip's own encoding (the RX8130CE takes a one-hot weekday, `weekday-one-hot` in its descriptor), and an RV-3028 whose descriptor carries `rv3028-eeprom-config` (the RAK12002 on a RAK4631) has that configuration, its backup switchover, stored in the chip's EEPROM. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again. +**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one and the time is within 2000-2099 (coalesced on the system work queue), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. A chip is adopted at boot only once it is identified (`adapters/clock/ZephyrRTCDiscover.c`), because other parts can answer at the same address: a device is passed over when two reads each show a bit set that the chip's data sheet fixes at zero, or a time field out of range, and a time is restored only from a read in which every field is valid. The time is written in each chip's own encoding (the RX8130CE takes a one-hot weekday, `weekday-one-hot` in its descriptor), and an RV-3028 whose descriptor carries `rv3028-eeprom-config` (the RAK12002 on a RAK4631) has that configuration, its backup switchover, stored in the chip's EEPROM. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again. **LED master switch** (`helpers/led_gate.{c,h}`, `set leds on|off`, all roles): one process-wide flag every LED driver consults — heartbeat and unread-message LEDs in `helpers/ui/ui_common.c`, the diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 90d8d3ad..485b86ec 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -911,8 +911,8 @@ config ZEPHCORE_RTC_AUTODISCOVER 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 has its time read and + On every GPS/app/CLI sync to a time within 2000-2099 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, a read identifies it, and a checked time read just before the store shows diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index d56c9d85..ca10b363 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -644,7 +644,7 @@ static bool rtc_probe(uint32_t *epoch_out) uint8_t db = blk[d->date_index] & 0x3F, ob = blk[5] & 0x1F, yb = blk[6]; #if RTC_RV3028_CFG - /* The mark, or a New Year's count on from it; FFh is a cut read. */ + /* A0h-FEh holds the A0h mark; FFh is a cut read. */ if (d->cfg != NULL && yb >= RV3028_YEAR_UNSET && yb != 0xFF) { LOG_INF("%s present, time not yet set (year %02Xh)", d->name, yb); continue; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 76ea11f3..ea29fe2a 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -32,7 +32,7 @@ extern "C" { * * A descriptor with rv3028-eeprom-config also has that config stored in the * chip's EEPROM, retried on the system work queue if it fails. There, a year - * of A0h-FEh gives no time: the A0h mark, or a New Year's count on from it. + * of A0h-FEh gives no time. */ bool zephcore_rtc_restore(uint32_t *epoch_out); From 04bf64dda5cd9cd966a2b34fa04614f7c4b41a00 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 14:38:20 -0700 Subject: [PATCH 12/14] rtc: say a refused RV3028 save marks the year, drop the FFh reason Local review: ARCHITECTURE.md said an out-of-range time is not written, but at an RV3028 with rv3028-eeprom-config its year is marked instead. A year of FFh reaching the A0h-FEh check has passed the BSF-checked read, so it is a stored value, not a cut read; the comment no longer gives a reason. A Kconfig help line the last edit left long is wrapped. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/Kconfig | 3 ++- zephcore/adapters/clock/ZephyrRTCDiscover.c | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e07f786b..34fc91f5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -860,7 +860,7 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC **Telemetry** (`REQ_TYPE_GET_TELEMETRY_DATA`, every role, upstream's shape): battery on channel 1, then `sensors.querySensors()` (`adapters/sensors/ZephyrSensorManager.cpp`): GPS position on channel 1 while the GPS is on and has a fix, each environment sensor and power-monitor channel found at boot on its own channel from 2 up (probe order), board-local analog sensors (T1000-E) on channel 1; then the MCU temperature on channel 1. Servers give guests battery + MCU temperature only and honour the requester's inverse permission mask, as upstream. Encoded by `helpers/compat/CayenneLPP.h`: upstream's library API and wire format, but values round to the nearest step where the library truncates. -**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one and the time is within 2000-2099 (coalesced on the system work queue), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. A chip is adopted at boot only once it is identified (`adapters/clock/ZephyrRTCDiscover.c`), because other parts can answer at the same address: a device is passed over when two reads each show a bit set that the chip's data sheet fixes at zero, or a time field out of range, and a time is restored only from a read in which every field is valid. The time is written in each chip's own encoding (the RX8130CE takes a one-hot weekday, `weekday-one-hot` in its descriptor), and an RV-3028 whose descriptor carries `rv3028-eeprom-config` (the RAK12002 on a RAK4631) has that configuration, its backup switchover, stored in the chip's EEPROM. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again. +**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one (coalesced on the system work queue; a time outside 2000-2099 is not written, and at an RV3028 with `rv3028-eeprom-config` its year is marked unset instead), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. A chip is adopted at boot only once it is identified (`adapters/clock/ZephyrRTCDiscover.c`), because other parts can answer at the same address: a device is passed over when two reads each show a bit set that the chip's data sheet fixes at zero, or a time field out of range, and a time is restored only from a read in which every field is valid. The time is written in each chip's own encoding (the RX8130CE takes a one-hot weekday, `weekday-one-hot` in its descriptor), and an RV-3028 whose descriptor carries `rv3028-eeprom-config` (the RAK12002 on a RAK4631) has that configuration, its backup switchover, stored in the chip's EEPROM. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again. **LED master switch** (`helpers/led_gate.{c,h}`, `set leds on|off`, all roles): one process-wide flag every LED driver consults — heartbeat and unread-message LEDs in `helpers/ui/ui_common.c`, the diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 485b86ec..0cd751c4 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -912,7 +912,8 @@ 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 to a time within 2000-2099 the time is - written back so it survives power-off. A node with rv3028-eeprom-config has its time read and + 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, a read identifies it, and a checked time read just before the store shows diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index ca10b363..fdbe54ce 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -644,7 +644,7 @@ static bool rtc_probe(uint32_t *epoch_out) uint8_t db = blk[d->date_index] & 0x3F, ob = blk[5] & 0x1F, yb = blk[6]; #if RTC_RV3028_CFG - /* A0h-FEh holds the A0h mark; FFh is a cut read. */ + /* A0h-FEh holds the A0h mark. */ if (d->cfg != NULL && yb >= RV3028_YEAR_UNSET && yb != 0xFF) { LOG_INF("%s present, time not yet set (year %02Xh)", d->name, yb); continue; From a91fb4c642b188af79b4ee436e9488df8471ced8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 7 Oct 2026 07:06:11 -0700 Subject: [PATCH 13/14] rtc: keep only the 2000-2099 write range, as the maintainer asked liquidraver/ZephCore#103 review (comment 6037017924): drop the A0h year mark, the one-burst write, rv3028_mark_year(), s_save_mark and the A0h read branch. The RV3028 write is dev's again (year 00h, 00h-05h, then the year): ZephCore's 2025 restore floor already rejects the year 2000 a cut write leaves. A time outside 2000-2099 is now not written for every chip, rv3028-eeprom-config included, and the chip keeps the time it has. The RV3028 retry stops if its run-on time leaves the range. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/adapters/clock/ZephyrRTCDiscover.c | 72 ++++++------------- zephcore/adapters/clock/ZephyrRTCDiscover.h | 10 ++- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 10 ++- 4 files changed, 29 insertions(+), 65 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 34fc91f5..b9365f15 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -860,7 +860,7 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC **Telemetry** (`REQ_TYPE_GET_TELEMETRY_DATA`, every role, upstream's shape): battery on channel 1, then `sensors.querySensors()` (`adapters/sensors/ZephyrSensorManager.cpp`): GPS position on channel 1 while the GPS is on and has a fix, each environment sensor and power-monitor channel found at boot on its own channel from 2 up (probe order), board-local analog sensors (T1000-E) on channel 1; then the MCU temperature on channel 1. Servers give guests battery + MCU temperature only and honour the requester's inverse permission mask, as upstream. Encoded by `helpers/compat/CayenneLPP.h`: upstream's library API and wire format, but values round to the nearest step where the library truncates. -**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one (coalesced on the system work queue; a time outside 2000-2099 is not written, and at an RV3028 with `rv3028-eeprom-config` its year is marked unset instead), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. A chip is adopted at boot only once it is identified (`adapters/clock/ZephyrRTCDiscover.c`), because other parts can answer at the same address: a device is passed over when two reads each show a bit set that the chip's data sheet fixes at zero, or a time field out of range, and a time is restored only from a read in which every field is valid. The time is written in each chip's own encoding (the RX8130CE takes a one-hot weekday, `weekday-one-hot` in its descriptor), and an RV-3028 whose descriptor carries `rv3028-eeprom-config` (the RAK12002 on a RAK4631) has that configuration, its backup switchover, stored in the chip's EEPROM. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again. +**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one (coalesced on the system work queue; a time outside 2000-2099 is not written), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. A chip is adopted at boot only once it is identified (`adapters/clock/ZephyrRTCDiscover.c`), because other parts can answer at the same address: a device is passed over when two reads each show a bit set that the chip's data sheet fixes at zero, or a time field out of range, and a time is restored only from a read in which every field is valid. The time is written in each chip's own encoding (the RX8130CE takes a one-hot weekday, `weekday-one-hot` in its descriptor), and an RV-3028 whose descriptor carries `rv3028-eeprom-config` (the RAK12002 on a RAK4631) has that configuration, its backup switchover, stored in the chip's EEPROM. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again. **LED master switch** (`helpers/led_gate.{c,h}`, `set leds on|off`, all roles): one process-wide flag every LED driver consults — heartbeat and unread-message LEDs in `helpers/ui/ui_common.c`, the diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index fdbe54ce..d439811d 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -225,7 +225,7 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) } /* The 7-byte time block for an epoch, in d's register order. False outside - * 2000-2099: ZephCore writes the year as two BCD digits and no century bit. */ + * 2000-2099: the year is written as two BCD digits with no century. */ static bool rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) { int y; @@ -281,27 +281,15 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) #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 RV3028_YEAR_UNSET 0xA0 /* not BCD; a year of A0h-FEh reads "not set" */ -/* Mark the year A0h, then write all seven registers in one access (4.5): - * a write cut between the mark and the year byte leaves the mark. */ +/* 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, 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, RV3028_YEAR_UNSET) == 0 && - i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 7) == 0; -} - -/* Mark the year alone, for a time outside 2000-2099, with the same BSF - * check as a time write. */ -static bool rv3028_mark_year(const struct rtc_desc *d) -{ - uint8_t st; - - return i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_STATUS, - RV3028_STATUS_BSF, 0) == 0 && - i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0 && - i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) == 0 && - !(st & RV3028_STATUS_BSF); + 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 @@ -546,7 +534,6 @@ static void rv3028_cfg_retry_fn(struct k_work *work) static uint32_t s_save_epoch; static int64_t s_save_at; static uint8_t s_save_tries; -static bool s_save_mark; /* the repeat writes the year mark, not a time */ static void rv3028_save_retry_fn(struct k_work *work); static K_WORK_DELAYABLE_DEFINE(s_save_retry, rv3028_save_retry_fn); @@ -555,25 +542,20 @@ static void rv3028_save_retry_fn(struct k_work *work) uint8_t blk[7]; ARG_UNUSED(work); - if (!s_save_mark && - !rtc_time_block(s_active, s_save_epoch + + if (!rtc_time_block(s_active, s_save_epoch + (uint32_t)((k_uptime_get() - s_save_at) / 1000), blk)) { - s_save_mark = true; /* the run-on time left 2000-2099 */ + LOG_WRN("RTC %s: time outside 2000-2099 not written", s_active->name); + return; } - if (s_save_mark ? !rv3028_mark_year(s_active) : !rv3028_steady(s_active, blk, true)) { + if (!rv3028_steady(s_active, blk, true)) { if (++s_save_tries < RTC_SAVE_RETRIES) { k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); } else { - LOG_WRN("RTC %s: save not confirmed after %u tries", + LOG_WRN("RTC %s: time write not confirmed after %u tries", s_active->name, s_save_tries + 1U); } return; } - if (s_save_mark) { - LOG_WRN("RTC %s: time outside 2000-2099 not written, year marked unset", - s_active->name); - return; - } rtc_clear_power_flag(s_active); LOG_INF("RTC %s: time write confirmed on retry", s_active->name); } @@ -643,14 +625,6 @@ static bool rtc_probe(uint32_t *epoch_out) uint8_t sb = blk[0] & 0x7F, mb = blk[1] & 0x7F, hb = blk[2] & 0x3F; uint8_t db = blk[d->date_index] & 0x3F, ob = blk[5] & 0x1F, yb = blk[6]; -#if RTC_RV3028_CFG - /* A0h-FEh holds the A0h mark. */ - if (d->cfg != NULL && yb >= RV3028_YEAR_UNSET && yb != 0xFF) { - LOG_INF("%s present, time not yet set (year %02Xh)", d->name, yb); - continue; - } -#endif - /* Identity leaves the hours and year unchecked, and may have let the * other fields pass on a flag read since; a time needs them all. */ if (!rtc_fields_ok(d, blk) || !bcd_field_ok(hb, 23) || !bcd_field_ok(yb, 99)) { @@ -709,31 +683,25 @@ void zephcore_rtc_save(uint32_t epoch) const struct rtc_desc *d = s_active; uint8_t blk[7]; - bool in_range = rtc_time_block(d, epoch, blk); + if (!rtc_time_block(d, epoch, blk)) { + LOG_WRN("RTC %s: time outside 2000-2099 not written", d->name); + return; + } #if RTC_RV3028_CFG if (d->cfg != NULL) { (void)k_work_cancel_delayable(&s_save_retry); s_save_epoch = epoch; s_save_at = k_uptime_get(); s_save_tries = 0; - s_save_mark = !in_range; - if (s_save_mark ? !rv3028_mark_year(d) : !rv3028_steady(d, blk, true)) { - LOG_WRN("RTC %s: %s not confirmed, repeating every 5 s", d->name, - s_save_mark ? "year mark" : "time write"); + 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; } - if (s_save_mark) { - LOG_WRN("RTC %s: time outside 2000-2099 not written, year marked " - "unset", d->name); - return; - } } else #endif - if (!in_range) { - LOG_WRN("RTC %s: time outside 2000-2099 not written", d->name); - return; - } else if (i2c_burst_write(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) != 0) { + 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; } diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index ea29fe2a..8198dd60 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -10,8 +10,8 @@ * * 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 - * ZephyrRTCClock::setCurrentTime() within 2000-2099 writes it back via - * zephcore_rtc_save(), so time survives the next power-off. + * ZephyrRTCClock::setCurrentTime() writes it back via zephcore_rtc_save(), so + * time survives the next power-off. */ #pragma once @@ -31,8 +31,7 @@ extern "C" { * present or none holds a trustworthy time. * * A descriptor with rv3028-eeprom-config also has that config stored in the - * chip's EEPROM, retried on the system work queue if it fails. There, a year - * of A0h-FEh gives no time. + * chip's EEPROM, retried on the system work queue if it fails. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -41,8 +40,7 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * power-loss flag. No-op if no RTC was discovered. Intended for real syncs * (GPS/app/CLI), not per-packet clock nudges. Call only from the system work * queue: an unconfirmed write is repeated there and shares state with this - * call. A time outside 2000-2099 is not written; at an rv3028-eeprom-config - * descriptor the year is marked A0h instead, so the next boot takes no time. + * call. A time outside 2000-2099 is not written. */ 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 72f17b11..efde7677 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -82,11 +82,9 @@ properties: 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 the year to A0h, - then all seven registers in one burst, year last, so a write cut - between the mark and the year byte gives no time at the next boot, - and a write not confirmed is repeated every 5 s, up to 12 times. A - time outside 2000-2099 is not written; the year is marked A0h - instead. Set it only on a board whose backup supply is known: + 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 bd0c46fc5f5784d426e3fa623c10119312a568d5 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 7 Oct 2026 07:09:40 -0700 Subject: [PATCH 14/14] rtc: say the binding and header summary write back only 2000-2099 Local review: the rv3028-eeprom-config text ("written on each sync") and the header's file summary still said every sync is written back. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.h | 4 ++-- zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 8198dd60..7b745ec0 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -10,8 +10,8 @@ * * 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 - * ZephyrRTCClock::setCurrentTime() writes it back via zephcore_rtc_save(), so - * time survives the next power-off. + * ZephyrRTCClock::setCurrentTime() within 2000-2099 writes it back via + * zephcore_rtc_save(), so time survives the next power-off. */ #pragma once diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index efde7677..505784c5 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -81,10 +81,10 @@ properties: 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. + boot and written on each sync within 2000-2099 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.