From dde2618a136ea6627ab223032183d37bd0bd869d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 10:33:46 -0700 Subject: [PATCH 01/19] rtc: select 24-hour mode before a time write, and take no time in 12-hour mode ZephCore assumed 24-hour mode and never selected it. If other firmware had left a chip in 12-hour mode, the read masked hours with 0x3F and took any BCD 00-23: PM 1-3 (21h-23h) were restored as 21:00-23:00, eight hours ahead, and PM 4-12 gave no time. The 24-hour hours byte a save wrote meant another hour to the chip. A new optional twelve-hour-bit descriptor property, , names the bit that reads set in 12-hour mode: Control 2 12_24 (10h bit 1) on the RV3028 (Application Manual Rev 1.4, p. 24), and hours bit 6 on the DS3231 node, which also covers the DS1307 (DS3231 19-5170 Rev 10, p. 12; DS1307 p. 8). The PCF8563, RX8130CE and RX8900 count 24 hours only. While the bit reads set, or cannot be read, the boot read takes no time, and a bit outside the time block is cleared; the RV3028 then converts its Hours register itself (02h, p. 15). Each time write selects 24-hour mode first, inside the RV3028's BSF bracket; on the DS3231 and DS1307 the write's own 24-hour hours byte clears the bit. Zephyr's rtc_rv3028 writes Control 2 = 00h at init, and MeshCore (meshcore-dev/MeshCore#3421) clears 12_24 on read and selects 24-hour mode before a write. Fixes ptr727/liquidraver-ZephCore#51. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 53 ++++++++++++++++++- zephcore/adapters/clock/ZephyrRTCDiscover.h | 10 ++-- zephcore/boards/common/rtc-i2c.dtsi | 3 ++ .../boards/nrf52840/rak4631/board.overlay | 1 + .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 10 ++++ 5 files changed, 71 insertions(+), 6 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 7f0b6b561..d43118f56 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -40,6 +40,8 @@ struct rtc_desc { uint8_t status_mask; /* "time unreliable" bit within status_reg */ const uint8_t *zero; /* 7 bytes of bits the data sheet shows as 0, or NULL */ bool week_one_hot; /* weekday as one bit per day, not 0-6 */ + uint8_t h12_reg; /* register of the 12-hour mode bit */ + uint8_t h12_mask; /* that bit, or 0 if the chip has no 12-hour mode */ #if RTC_RV3028_CFG const uint8_t *cfg; /* rv3028-eeprom-config triplets, or NULL */ uint8_t cfg_len; @@ -91,6 +93,12 @@ DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_CFG_ARRAY) DT_NODE_HAS_PROP(node, zero_mask), \ (RTC_ZERO_NAME(node)), (NULL)), \ .week_one_hot = DT_PROP(node, weekday_one_hot), \ + .h12_reg = COND_CODE_1( \ + DT_NODE_HAS_PROP(node, twelve_hour_bit), \ + (DT_PROP_BY_IDX(node, twelve_hour_bit, 0)), (0)), \ + .h12_mask = COND_CODE_1( \ + DT_NODE_HAS_PROP(node, twelve_hour_bit), \ + (DT_PROP_BY_IDX(node, twelve_hour_bit, 1)), (0)), \ RTC_CFG_FIELDS(node) \ .name = DT_NODE_FULL_NAME(node), \ }, @@ -264,6 +272,36 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) } } +/* 1 if the chip counts hours in 12-hour mode, 0 if in 24-hour mode or it has + * none, -1 if a mode bit outside the time block could not be read. */ +static int rtc_12h(const struct rtc_desc *d, const uint8_t blk[7]) +{ + uint8_t v; + + if (d->h12_mask == 0) { + return 0; + } + if (d->h12_reg >= d->time_reg && d->h12_reg < d->time_reg + 7) { + return (blk[d->h12_reg - d->time_reg] & d->h12_mask) != 0; + } + if (i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &v) != 0) { + return -1; + } + return (v & d->h12_mask) != 0; +} + +/* Select 24-hour mode. A mode bit outside the time block is cleared, and the + * RV3028 then converts its Hours register itself (02h, p. 15). One inside it + * (DS3231/DS1307 hours bit 6) is cleared by the time write's hours byte. */ +static bool rtc_select_24h(const struct rtc_desc *d) +{ + if (d->h12_mask == 0 || + (d->h12_reg >= d->time_reg && d->h12_reg < d->time_reg + 7)) { + return true; + } + return i2c_reg_update_byte(d->bus, d->addr, d->h12_reg, d->h12_mask, 0) == 0; +} + #if RTC_RV3028_CFG /* RV3028 registers for reading and writing its configuration EEPROM. */ @@ -287,7 +325,8 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) * 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 && + return rtc_select_24h(d) && + 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; } @@ -616,6 +655,15 @@ static bool rtc_probe(uint32_t *epoch_out) "next GPS/app/CLI sync", d->name); continue; } + int h12 = rtc_12h(d, blk); + + if (h12 != 0) { + (void)rtc_select_24h(d); + LOG_WRN("%s present, %s — clock will be set on the next " + "GPS/app/CLI sync", d->name, + h12 > 0 ? "12-hour mode" : "hour mode unreadable"); + continue; + } int flag = rtc_power_flag(d, blk); if (flag != 0) { @@ -701,7 +749,8 @@ void zephcore_rtc_save(uint32_t epoch) } } else #endif - if (i2c_burst_write(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) != 0) { + if (!rtc_select_24h(d) || + 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 c2d22aebe..a3334e0db 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -35,8 +35,10 @@ 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). Returns - * false if none present or no trustworthy time is held. + * hours and year are each in range, and never while the descriptor's + * twelve-hour-bit reads set or cannot be read; a set bit outside the time + * block is then cleared. 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 @@ -57,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 to the discovered RTC chip, selecting + * 24-hour mode first, 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. diff --git a/zephcore/boards/common/rtc-i2c.dtsi b/zephcore/boards/common/rtc-i2c.dtsi index 2dabd133a..caff4dad1 100644 --- a/zephcore/boards/common/rtc-i2c.dtsi +++ b/zephcore/boards/common/rtc-i2c.dtsi @@ -39,6 +39,8 @@ rtc_ds3231: rtc-ds3231@68 { date-index = <4>; status-reg = <0x0F>; /* control/status */ status-mask = <0x80>; /* OSF — oscillator stopped */ + /* Hours bit 6, 12-hour mode: DS3231 (p. 12), DS1307 (p. 8) */ + twelve-hour-bit = <0x02 0x40>; /* Bits shown as 0 in all three parts: DS3231 (19-5170 Rev 10, Figure 1, * p. 11) and DS3232 (19-5337 Rev 5, Figure 1, p. 11), where 05h bit 7 is * Century, and DS1307 (Rev 3/15, Table 2, p. 8, "always reads back as @@ -65,6 +67,7 @@ rtc_rv3028: rtc-rv3028@52 { date-index = <4>; status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ + twelve-hour-bit = <0x10 0x02>; /* Control 2 12_24, p. 24 */ /* RV-3028-C7 Application Manual Rev. 1.4, 3.2, p. 12 */ zero-mask = [80 80 c0 f8 c0 e0 00]; }; diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index 02ef9ed06..ea9360899 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -228,6 +228,7 @@ date-index = <4>; status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ + twelve-hour-bit = <0x10 0x02>; /* Control 2 12_24, p. 24 */ /* RV-3028-C7 Application Manual Rev. 1.4, 3.2, p. 12 */ zero-mask = [80 80 c0 f8 c0 e0 00]; /* The RAK12002's backup is a supercapacitor. As the RV-3028-C7 diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index efde76778..8a6c90727 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -62,6 +62,16 @@ properties: bit 6) rather than a number 0-6. Set it for a chip whose data sheet says so, such as the RX8130CE. + twelve-hour-bit: + type: array + description: > + : the bit that reads set while the chip counts hours + in 12-hour mode. Leave it out for a chip with no 12-hour mode. While + it reads set, or cannot be read, no time is taken from the chip, and + each time write selects 24-hour mode first: a bit outside the time + block is cleared, and one inside it is cleared by the write's own + hours byte. + rv3028-eeprom-config: type: uint8-array description: > From 52d83b9902c81be268a3fe56cde57778548cf658 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 10:47:29 -0700 Subject: [PATCH 02/19] rtc: clear the 12-hour bit only on the adopted chip, and never from a bad read Local review: the boot-time clear ran on any present candidate the probe reached, so a device at the RTC's address that ZephCore never adopts could be written. It now runs on the adopted chip only, as the RV3028 config store does. The clear reads the register and refuses a read of all 1s rather than writing it back, since that is a transfer cut short: on the RV3028 it would set every other Control 2 bit, and RESET always reads 0 there. twelve-hour-bit is a two-byte uint8-array with a length assert, so a malformed node fails the build. The binding now says the boot clears the bit, and a header line the first edit left unwrapped is rewrapped. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 24 ++++++++++++++++--- zephcore/adapters/clock/ZephyrRTCDiscover.h | 7 +++--- zephcore/boards/common/rtc-i2c.dtsi | 4 ++-- .../boards/nrf52840/rak4631/board.overlay | 2 +- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 13 +++++----- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index d43118f56..562258406 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -60,6 +60,13 @@ struct rtc_desc { DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_ZERO_ARRAY) +#define RTC_H12_CHECK(node) \ + IF_ENABLED(DT_NODE_HAS_PROP(node, twelve_hour_bit), \ + (BUILD_ASSERT(DT_PROP_LEN(node, twelve_hour_bit) == 2, \ + "twelve-hour-bit is [register mask]");)) + +DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_H12_CHECK) + #if RTC_RV3028_CFG #define RTC_CFG_NAME(node) _CONCAT(rtc_cfg_, DT_DEP_ORD(node)) @@ -292,14 +299,23 @@ static int rtc_12h(const struct rtc_desc *d, const uint8_t blk[7]) /* Select 24-hour mode. A mode bit outside the time block is cleared, and the * RV3028 then converts its Hours register itself (02h, p. 15). One inside it - * (DS3231/DS1307 hours bit 6) is cleared by the time write's hours byte. */ + * (DS3231/DS1307 hours bit 6) is cleared by the time write's hours byte. A + * read of all 1s, a transfer cut short, is not written back: on the RV3028 + * it would set every other Control 2 bit, and RESET always reads 0 there. */ static bool rtc_select_24h(const struct rtc_desc *d) { + uint8_t v; + if (d->h12_mask == 0 || (d->h12_reg >= d->time_reg && d->h12_reg < d->time_reg + 7)) { return true; } - return i2c_reg_update_byte(d->bus, d->addr, d->h12_reg, d->h12_mask, 0) == 0; + if (i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &v) != 0 || v == 0xFF) { + return false; + } + return !(v & d->h12_mask) || + i2c_reg_write_byte(d->bus, d->addr, d->h12_reg, + v & (uint8_t)~d->h12_mask) == 0; } #if RTC_RV3028_CFG @@ -658,7 +674,9 @@ static bool rtc_probe(uint32_t *epoch_out) int h12 = rtc_12h(d, blk); if (h12 != 0) { - (void)rtc_select_24h(d); + if (d == s_active) { + (void)rtc_select_24h(d); /* only the adopted chip */ + } LOG_WRN("%s present, %s — clock will be set on the next " "GPS/app/CLI sync", d->name, h12 > 0 ? "12-hour mode" : "hour mode unreadable"); diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index a3334e0db..41745b6a7 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -37,7 +37,7 @@ extern "C" { * rules a device out. A time is taken only from a clean read whose fields, * hours and year are each in range, and never while the descriptor's * twelve-hour-bit reads set or cannot be read; a set bit outside the time - * block is then cleared. Returns false if none present or no trustworthy + * block is then cleared on the adopted chip. Returns false if none present or no trustworthy * time is held. * * At a descriptor with rv3028-eeprom-config, the time is read once more @@ -60,8 +60,9 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); /* * Persist an authoritative epoch to the discovered RTC chip, selecting - * 24-hour mode first, 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. + * 24-hour mode first, 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. diff --git a/zephcore/boards/common/rtc-i2c.dtsi b/zephcore/boards/common/rtc-i2c.dtsi index caff4dad1..f3a206634 100644 --- a/zephcore/boards/common/rtc-i2c.dtsi +++ b/zephcore/boards/common/rtc-i2c.dtsi @@ -40,7 +40,7 @@ rtc_ds3231: rtc-ds3231@68 { status-reg = <0x0F>; /* control/status */ status-mask = <0x80>; /* OSF — oscillator stopped */ /* Hours bit 6, 12-hour mode: DS3231 (p. 12), DS1307 (p. 8) */ - twelve-hour-bit = <0x02 0x40>; + twelve-hour-bit = [02 40]; /* Bits shown as 0 in all three parts: DS3231 (19-5170 Rev 10, Figure 1, * p. 11) and DS3232 (19-5337 Rev 5, Figure 1, p. 11), where 05h bit 7 is * Century, and DS1307 (Rev 3/15, Table 2, p. 8, "always reads back as @@ -67,7 +67,7 @@ rtc_rv3028: rtc-rv3028@52 { date-index = <4>; status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ - twelve-hour-bit = <0x10 0x02>; /* Control 2 12_24, p. 24 */ + twelve-hour-bit = [10 02]; /* Control 2 12_24, p. 24 */ /* RV-3028-C7 Application Manual Rev. 1.4, 3.2, p. 12 */ zero-mask = [80 80 c0 f8 c0 e0 00]; }; diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index ea9360899..450f41f23 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -228,7 +228,7 @@ date-index = <4>; status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ - twelve-hour-bit = <0x10 0x02>; /* Control 2 12_24, p. 24 */ + twelve-hour-bit = [10 02]; /* Control 2 12_24, p. 24 */ /* RV-3028-C7 Application Manual Rev. 1.4, 3.2, p. 12 */ zero-mask = [80 80 c0 f8 c0 e0 00]; /* The RAK12002's backup is a supercapacitor. As the RV-3028-C7 diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 8a6c90727..c2a4da2ed 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -63,14 +63,15 @@ properties: says so, such as the RX8130CE. twelve-hour-bit: - type: array + type: uint8-array description: > - : the bit that reads set while the chip counts hours + [register mask]: the bit that reads set while the chip counts hours in 12-hour mode. Leave it out for a chip with no 12-hour mode. While - it reads set, or cannot be read, no time is taken from the chip, and - each time write selects 24-hour mode first: a bit outside the time - block is cleared, and one inside it is cleared by the write's own - hours byte. + it reads set, or cannot be read, no time is taken from the chip; a + set bit outside the time block is then cleared at boot on the + adopted chip. Each time write selects 24-hour mode first: a bit + outside the time block is cleared, and one inside it is cleared by + the write's own hours byte. rv3028-eeprom-config: type: uint8-array From e6d568b30949a2973985e32abb70b6c9ff2357a0 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 11:06:25 -0700 Subject: [PATCH 03/19] rtc: read an all-1s 12-hour mode register as unreadable Local review: rtc_12h() took a read of all 1s as 12-hour mode while rtc_select_24h() refuses the same read as a transfer cut short, so a switchover during the boot read of RV3028 Control 2, which never reads 0xFF since RESET always reads 0, logged "12-hour mode" rather than "hour mode unreadable". No time is taken either way. A header line the previous commit left unwrapped is reflowed. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 5 +++-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 562258406..e7c4b5a5b 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -280,7 +280,8 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) } /* 1 if the chip counts hours in 12-hour mode, 0 if in 24-hour mode or it has - * none, -1 if a mode bit outside the time block could not be read. */ + * none, -1 if a mode bit outside the time block could not be read, or read + * as all 1s, a transfer cut short (see rtc_select_24h()). */ static int rtc_12h(const struct rtc_desc *d, const uint8_t blk[7]) { uint8_t v; @@ -291,7 +292,7 @@ static int rtc_12h(const struct rtc_desc *d, const uint8_t blk[7]) if (d->h12_reg >= d->time_reg && d->h12_reg < d->time_reg + 7) { return (blk[d->h12_reg - d->time_reg] & d->h12_mask) != 0; } - if (i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &v) != 0) { + if (i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &v) != 0 || v == 0xFF) { return -1; } return (v & d->h12_mask) != 0; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 41745b6a7..ca252c2a2 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -37,8 +37,8 @@ extern "C" { * rules a device out. A time is taken only from a clean read whose fields, * hours and year are each in range, and never while the descriptor's * twelve-hour-bit reads set or cannot be read; a set bit outside the time - * block is then cleared on the adopted chip. Returns false if none present or no trustworthy - * time is held. + * block is then cleared on the adopted chip. 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 From 235c3c00511ad1daeed3cedb75cbac4083fe2aa8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 11:14:18 -0700 Subject: [PATCH 04/19] rtc: refuse a 12-hour mode register read that shows a bit reading 0 Local review: a one-byte read cut mid-byte by a switchover returns the real high bits and then 1s, with no I2C error and not always 0xFF, so the 0xFF check let a torn RV3028 Control 2 read through. If bit 1 was in the 1s tail, the clear wrote back RESET and the trailing enable bits. twelve-hour-bit gains a third byte naming the register's bits that always read 0 (RESET, Control 2 bit 0, p. 24); a read showing one is cut short, gives no time and is never written back. Every cut read ends in a 1 on bit 0, so this covers the 0xFF case too. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 28 +++++++++++++------ zephcore/boards/common/rtc-i2c.dtsi | 7 +++-- .../boards/nrf52840/rak4631/board.overlay | 2 +- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 18 +++++++----- 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index e7c4b5a5b..6e4ed6339 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -42,6 +42,7 @@ struct rtc_desc { bool week_one_hot; /* weekday as one bit per day, not 0-6 */ uint8_t h12_reg; /* register of the 12-hour mode bit */ uint8_t h12_mask; /* that bit, or 0 if the chip has no 12-hour mode */ + uint8_t h12_zero; /* bits of h12_reg that always read 0 */ #if RTC_RV3028_CFG const uint8_t *cfg; /* rv3028-eeprom-config triplets, or NULL */ uint8_t cfg_len; @@ -62,8 +63,8 @@ DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_ZERO_ARRAY) #define RTC_H12_CHECK(node) \ IF_ENABLED(DT_NODE_HAS_PROP(node, twelve_hour_bit), \ - (BUILD_ASSERT(DT_PROP_LEN(node, twelve_hour_bit) == 2, \ - "twelve-hour-bit is [register mask]");)) + (BUILD_ASSERT(DT_PROP_LEN(node, twelve_hour_bit) == 3, \ + "twelve-hour-bit is [register mask zero]");)) DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_H12_CHECK) @@ -106,6 +107,9 @@ DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_CFG_ARRAY) .h12_mask = COND_CODE_1( \ DT_NODE_HAS_PROP(node, twelve_hour_bit), \ (DT_PROP_BY_IDX(node, twelve_hour_bit, 1)), (0)), \ + .h12_zero = COND_CODE_1( \ + DT_NODE_HAS_PROP(node, twelve_hour_bit), \ + (DT_PROP_BY_IDX(node, twelve_hour_bit, 2)), (0)), \ RTC_CFG_FIELDS(node) \ .name = DT_NODE_FULL_NAME(node), \ }, @@ -279,9 +283,18 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) } } +/* Read the register holding an out-of-block 12-hour bit. False on a failed + * read, or one showing a bit that always reads 0: a read cut short ends in + * 1s (the RV3028's RESET, Control 2 bit 0, always reads 0, p. 24). */ +static bool rtc_read_h12_reg(const struct rtc_desc *d, uint8_t *v) +{ + return i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, v) == 0 && + !(*v & d->h12_zero); +} + /* 1 if the chip counts hours in 12-hour mode, 0 if in 24-hour mode or it has - * none, -1 if a mode bit outside the time block could not be read, or read - * as all 1s, a transfer cut short (see rtc_select_24h()). */ + * none, -1 if a mode bit outside the time block could not be read cleanly + * (see rtc_read_h12_reg()). */ static int rtc_12h(const struct rtc_desc *d, const uint8_t blk[7]) { uint8_t v; @@ -292,7 +305,7 @@ static int rtc_12h(const struct rtc_desc *d, const uint8_t blk[7]) if (d->h12_reg >= d->time_reg && d->h12_reg < d->time_reg + 7) { return (blk[d->h12_reg - d->time_reg] & d->h12_mask) != 0; } - if (i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &v) != 0 || v == 0xFF) { + if (!rtc_read_h12_reg(d, &v)) { return -1; } return (v & d->h12_mask) != 0; @@ -301,8 +314,7 @@ static int rtc_12h(const struct rtc_desc *d, const uint8_t blk[7]) /* Select 24-hour mode. A mode bit outside the time block is cleared, and the * RV3028 then converts its Hours register itself (02h, p. 15). One inside it * (DS3231/DS1307 hours bit 6) is cleared by the time write's hours byte. A - * read of all 1s, a transfer cut short, is not written back: on the RV3028 - * it would set every other Control 2 bit, and RESET always reads 0 there. */ + * read that was not clean is never written back. */ static bool rtc_select_24h(const struct rtc_desc *d) { uint8_t v; @@ -311,7 +323,7 @@ static bool rtc_select_24h(const struct rtc_desc *d) (d->h12_reg >= d->time_reg && d->h12_reg < d->time_reg + 7)) { return true; } - if (i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &v) != 0 || v == 0xFF) { + if (!rtc_read_h12_reg(d, &v)) { return false; } return !(v & d->h12_mask) || diff --git a/zephcore/boards/common/rtc-i2c.dtsi b/zephcore/boards/common/rtc-i2c.dtsi index f3a206634..46614cd58 100644 --- a/zephcore/boards/common/rtc-i2c.dtsi +++ b/zephcore/boards/common/rtc-i2c.dtsi @@ -39,8 +39,9 @@ rtc_ds3231: rtc-ds3231@68 { date-index = <4>; status-reg = <0x0F>; /* control/status */ status-mask = <0x80>; /* OSF — oscillator stopped */ - /* Hours bit 6, 12-hour mode: DS3231 (p. 12), DS1307 (p. 8) */ - twelve-hour-bit = [02 40]; + /* Hours bit 6, 12-hour mode: DS3231 (p. 12), DS1307 (p. 8). In the + * time block, so the zero byte is unused. */ + twelve-hour-bit = [02 40 00]; /* Bits shown as 0 in all three parts: DS3231 (19-5170 Rev 10, Figure 1, * p. 11) and DS3232 (19-5337 Rev 5, Figure 1, p. 11), where 05h bit 7 is * Century, and DS1307 (Rev 3/15, Table 2, p. 8, "always reads back as @@ -67,7 +68,7 @@ rtc_rv3028: rtc-rv3028@52 { date-index = <4>; status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ - twelve-hour-bit = [10 02]; /* Control 2 12_24, p. 24 */ + twelve-hour-bit = [10 02 01]; /* Control 2 12_24; RESET reads 0, p. 24 */ /* RV-3028-C7 Application Manual Rev. 1.4, 3.2, p. 12 */ zero-mask = [80 80 c0 f8 c0 e0 00]; }; diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index 450f41f23..01a54d5c9 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -228,7 +228,7 @@ date-index = <4>; status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ - twelve-hour-bit = [10 02]; /* Control 2 12_24, p. 24 */ + twelve-hour-bit = [10 02 01]; /* Control 2 12_24; RESET reads 0, p. 24 */ /* RV-3028-C7 Application Manual Rev. 1.4, 3.2, p. 12 */ zero-mask = [80 80 c0 f8 c0 e0 00]; /* The RAK12002's backup is a supercapacitor. As the RV-3028-C7 diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index c2a4da2ed..127c60d1f 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -65,13 +65,17 @@ properties: twelve-hour-bit: type: uint8-array description: > - [register mask]: the bit that reads set while the chip counts hours - in 12-hour mode. Leave it out for a chip with no 12-hour mode. While - it reads set, or cannot be read, no time is taken from the chip; a - set bit outside the time block is then cleared at boot on the - adopted chip. Each time write selects 24-hour mode first: a bit - outside the time block is cleared, and one inside it is cleared by - the write's own hours byte. + [register mask zero]: the bit that reads set while the chip counts + hours in 12-hour mode, and the bits of that register that always + read 0. Leave it out for a chip with no 12-hour mode. For a + register outside the time block, a read showing a zero bit set is + taken as cut short (it ends in 1s), so name at least bit 0 there; + it is never written back. While the mode bit reads set, or cannot + be read cleanly, no time is taken from the chip; a set bit outside + the time block is then cleared at boot on the adopted chip. Each + time write selects 24-hour mode first: a bit outside the time + block is cleared, and one inside it is cleared by the write's own + hours byte. rv3028-eeprom-config: type: uint8-array From d927e9330eed6c5cbbfd82f7226e18a545f58754 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 14:19:50 -0700 Subject: [PATCH 05/19] rtc: require a zero bit for an out-of-block twelve-hour-bit Local review: the binding asked for at least one always-zero bit when the mode register sits outside the time block, but nothing enforced it, and with none every read counts as clean, so a cut read could be written back. A BUILD_ASSERT now requires it (confirmed to fail on [10 02 00]). The binding says a read showing a zero bit set is never written back, rather than leaving "it" to refer to bit 0. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 7 ++++++- zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml | 16 ++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index c1fa29514..0c515e387 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -61,10 +61,15 @@ struct rtc_desc { DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_ZERO_ARRAY) +#define RTC_H12_REG(node) DT_PROP_BY_IDX(node, twelve_hour_bit, 0) #define RTC_H12_CHECK(node) \ IF_ENABLED(DT_NODE_HAS_PROP(node, twelve_hour_bit), \ (BUILD_ASSERT(DT_PROP_LEN(node, twelve_hour_bit) == 3, \ - "twelve-hour-bit is [register mask zero]");)) + "twelve-hour-bit is [register mask zero]"); \ + BUILD_ASSERT((RTC_H12_REG(node) >= DT_PROP(node, time_reg) && \ + RTC_H12_REG(node) < DT_PROP(node, time_reg) + 7) || \ + DT_PROP_BY_IDX(node, twelve_hour_bit, 2) != 0, \ + "twelve-hour-bit outside the time block needs a zero bit");)) DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_H12_CHECK) diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 127c60d1f..0395339b6 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -68,14 +68,14 @@ properties: [register mask zero]: the bit that reads set while the chip counts hours in 12-hour mode, and the bits of that register that always read 0. Leave it out for a chip with no 12-hour mode. For a - register outside the time block, a read showing a zero bit set is - taken as cut short (it ends in 1s), so name at least bit 0 there; - it is never written back. While the mode bit reads set, or cannot - be read cleanly, no time is taken from the chip; a set bit outside - the time block is then cleared at boot on the adopted chip. Each - time write selects 24-hour mode first: a bit outside the time - block is cleared, and one inside it is cleared by the write's own - hours byte. + register outside the time block the zero byte must name at least + one bit (the build checks): a read showing one set was cut short, + since a cut read ends in 1s, and such a read is never written + back. While the mode bit reads set, or cannot be read cleanly, no + time is taken from the chip; a set bit outside the time block is + then cleared at boot on the adopted chip. Each time write selects + 24-hour mode first: a bit outside the time block is cleared, and + one inside it is cleared by the write's own hours byte. rv3028-eeprom-config: type: uint8-array From 70def664439b3e6ee28e57aa8ed7f3ea3f2bbc8c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 14:39:49 -0700 Subject: [PATCH 06/19] rtc: leave the 12-hour bit to the next save, and reject a zero byte over it Local review: the boot-time clear of an out-of-block 12-hour bit was the one write on the RV3028 boot path outside the BSF bracket, so a switchover during it could leave a corrupt Control 2 that no later read-modify-write repairs. It is removed: a chip in 12-hour mode gives no time until the next sync, whose write selects 24-hour mode inside the bracket. A BUILD_ASSERT also rejects a zero byte that overlaps the mode bit (confirmed to fail on [10 02 03]), which would refuse every read in 12-hour mode and fail every save. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 8 ++++---- zephcore/adapters/clock/ZephyrRTCDiscover.h | 3 +-- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 18 +++++++++--------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 0c515e387..0ac702aa9 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -69,7 +69,10 @@ DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_ZERO_ARRAY) BUILD_ASSERT((RTC_H12_REG(node) >= DT_PROP(node, time_reg) && \ RTC_H12_REG(node) < DT_PROP(node, time_reg) + 7) || \ DT_PROP_BY_IDX(node, twelve_hour_bit, 2) != 0, \ - "twelve-hour-bit outside the time block needs a zero bit");)) + "twelve-hour-bit outside the time block needs a zero bit"); \ + BUILD_ASSERT((DT_PROP_BY_IDX(node, twelve_hour_bit, 1) & \ + DT_PROP_BY_IDX(node, twelve_hour_bit, 2)) == 0, \ + "twelve-hour-bit zero bits overlap the mode bit");)) DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_H12_CHECK) @@ -677,9 +680,6 @@ static bool rtc_probe(uint32_t *epoch_out) int h12 = rtc_12h(d, blk); if (h12 != 0) { - if (d == s_active) { - (void)rtc_select_24h(d); /* only the adopted chip */ - } LOG_WRN("%s present, %s — clock will be set on the next " "GPS/app/CLI sync", d->name, h12 > 0 ? "12-hour mode" : "hour mode unreadable"); diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index d897e6c2c..77e3cf242 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -28,8 +28,7 @@ extern "C" { * sane time (year >= 2025 and its power-loss flag clear): store its Unix * epoch in *epoch_out and return true. The first chip found (valid time or * not) is remembered as the write-back target. No time is taken while the - * descriptor's twelve-hour-bit reads set or cannot be read cleanly; a set - * bit outside the time block is then cleared on the adopted chip. Returns + * descriptor's twelve-hour-bit reads set or cannot be read cleanly. Returns * false if none is present or none holds a trustworthy time. * * A descriptor with rv3028-eeprom-config also has that config stored in the diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 0395339b6..a58814074 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -67,15 +67,15 @@ properties: description: > [register mask zero]: the bit that reads set while the chip counts hours in 12-hour mode, and the bits of that register that always - read 0. Leave it out for a chip with no 12-hour mode. For a - register outside the time block the zero byte must name at least - one bit (the build checks): a read showing one set was cut short, - since a cut read ends in 1s, and such a read is never written - back. While the mode bit reads set, or cannot be read cleanly, no - time is taken from the chip; a set bit outside the time block is - then cleared at boot on the adopted chip. Each time write selects - 24-hour mode first: a bit outside the time block is cleared, and - one inside it is cleared by the write's own hours byte. + read 0, which must not include the mode bit. Leave it out for a + chip with no 12-hour mode. For a register outside the time block + the zero byte must name at least one bit (the build checks both): + a read showing one set was cut short, since a cut read ends in 1s, + and such a read is never written back. While the mode bit reads + set, or cannot be read cleanly, no time is taken from the chip. + Each time write selects 24-hour mode first: a bit outside the time + block is cleared, and one inside it is cleared by the write's own + hours byte. rv3028-eeprom-config: type: uint8-array From 9a42030f3341eb1d66970a46aad0f741b0496e09 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 14:54:00 -0700 Subject: [PATCH 07/19] rtc: require bit 0 out of block, the hours register in block, and a mode bit Local review: only bit 0 reads 1 in every read cut short (I2C sends the MSB first), so an out-of-block zero byte naming another bit could let a torn read be written back; the build now requires bit 0. An in-block twelve-hour-bit must be the hours register, which the time write clears, and the mode mask must be nonzero. Each was confirmed to fail the build ([10 02 80], [04 10 00], [10 00 01]). The rv3028_write_time() comment and the binding no longer call year 00h the first write, since 24-hour mode is selected before it. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 17 +++++++---- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 28 +++++++++---------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 0ac702aa9..33dc86731 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -66,10 +66,16 @@ DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_ZERO_ARRAY) IF_ENABLED(DT_NODE_HAS_PROP(node, twelve_hour_bit), \ (BUILD_ASSERT(DT_PROP_LEN(node, twelve_hour_bit) == 3, \ "twelve-hour-bit is [register mask zero]"); \ + BUILD_ASSERT(DT_PROP_BY_IDX(node, twelve_hour_bit, 1) != 0, \ + "twelve-hour-bit needs a mode bit"); \ + BUILD_ASSERT(!(RTC_H12_REG(node) >= DT_PROP(node, time_reg) && \ + RTC_H12_REG(node) < DT_PROP(node, time_reg) + 7) || \ + RTC_H12_REG(node) == DT_PROP(node, time_reg) + 2, \ + "twelve-hour-bit in the time block must be in hours"); \ BUILD_ASSERT((RTC_H12_REG(node) >= DT_PROP(node, time_reg) && \ RTC_H12_REG(node) < DT_PROP(node, time_reg) + 7) || \ - DT_PROP_BY_IDX(node, twelve_hour_bit, 2) != 0, \ - "twelve-hour-bit outside the time block needs a zero bit"); \ + (DT_PROP_BY_IDX(node, twelve_hour_bit, 2) & 0x01), \ + "twelve-hour-bit outside the time block needs bit 0 as a zero bit"); \ BUILD_ASSERT((DT_PROP_BY_IDX(node, twelve_hour_bit, 1) & \ DT_PROP_BY_IDX(node, twelve_hour_bit, 2)) == 0, \ "twelve-hour-bit zero bits overlap the mode bit");)) @@ -348,9 +354,10 @@ static bool rtc_select_24h(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 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. */ +/* Year 00h before the other time bytes and the real year last (24-hour mode + * is selected before both): 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 rtc_select_24h(d) && diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index a58814074..46019325e 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -68,14 +68,14 @@ properties: [register mask zero]: the bit that reads set while the chip counts hours in 12-hour mode, and the bits of that register that always read 0, which must not include the mode bit. Leave it out for a - chip with no 12-hour mode. For a register outside the time block - the zero byte must name at least one bit (the build checks both): - a read showing one set was cut short, since a cut read ends in 1s, - and such a read is never written back. While the mode bit reads - set, or cannot be read cleanly, no time is taken from the chip. - Each time write selects 24-hour mode first: a bit outside the time - block is cleared, and one inside it is cleared by the write's own - hours byte. + chip with no 12-hour mode. Inside the time block it must be the + hours register. Outside it, the zero bits must include bit 0 (the + build checks these): a read cut short ends in 1s, so it always + shows bit 0 set, and such a read is never written back. While the + mode bit reads set, or cannot be read cleanly, no time is taken + from the chip. Each time write selects 24-hour mode first: a bit + outside the time block is cleared, and one in hours is cleared by + the write's own hours byte. rv3028-eeprom-config: type: uint8-array @@ -97,9 +97,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: - trickle charging suits a supercapacitor or rechargeable cell, - never a primary cell. + cleared before and checked after. A write sets year 00h before the + other time bytes 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 59ba354daf45b44fc3ed48c9df152fe7de24990b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 15:24:32 -0700 Subject: [PATCH 08/19] rtc: honour twelve-hour-bit's zero bits in the time block too Copilot (PR 52): an in-block mode register checked only the mode bit, ignoring the declared always-zero bits, so the binding's "cannot be read cleanly" rule held only outside the time block. rtc_12h() now gives -1 for a set zero bit wherever the register is. In-tree this changes nothing: the DS3231 node declares no in-block zero bits, and its zero-mask already rules out a block with hours bit 7 set. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 33dc86731..17ed78a00 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -309,9 +309,11 @@ static int rtc_12h(const struct rtc_desc *d, const uint8_t blk[7]) return 0; } if (d->h12_reg >= d->time_reg && d->h12_reg < d->time_reg + 7) { - return (blk[d->h12_reg - d->time_reg] & d->h12_mask) != 0; + v = blk[d->h12_reg - d->time_reg]; + } else if (!rtc_read_h12_reg(d, &v)) { + return -1; } - if (!rtc_read_h12_reg(d, &v)) { + if (v & d->h12_zero) { return -1; } return (v & d->h12_mask) != 0; From 74c7e0cf070a3825e9fd07838a724ee8e98a7db0 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 15:31:19 -0700 Subject: [PATCH 09/19] rtc: read back the 12-hour clear and repair a torn write Local review: the save's Control 2 read-modify-write was never checked against the value it meant to write. On an RV3028 without rv3028-eeprom-config there is no BSF bracket at all, and even inside the bracket a retry checks bit 1 alone, so a write torn by a switchover (5.10) could leave other Control 2 bits set for good. rtc_select_24h() now reads the register back and writes the intended value once more if it differs, failing the save if it still does. The DS3231 node's comment no longer calls its zero byte unused, which 59ba354 made untrue. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 21 ++++++++++++++++----- zephcore/boards/common/rtc-i2c.dtsi | 4 ++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 17ed78a00..3285d4d7f 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -321,10 +321,11 @@ static int rtc_12h(const struct rtc_desc *d, const uint8_t blk[7]) /* Select 24-hour mode: clear an out-of-block bit (the RV3028 converts its * Hours itself, 02h); an in-block one (DS3231 hours bit 6) is cleared by the - * time write's hours byte. */ + * time write's hours byte. The clear is read back and written once more if + * it differs, which repairs a write torn by a switchover (5.10). */ static bool rtc_select_24h(const struct rtc_desc *d) { - uint8_t v; + uint8_t v, w, r; if (d->h12_mask == 0 || (d->h12_reg >= d->time_reg && d->h12_reg < d->time_reg + 7)) { @@ -333,9 +334,19 @@ static bool rtc_select_24h(const struct rtc_desc *d) if (!rtc_read_h12_reg(d, &v)) { return false; } - return !(v & d->h12_mask) || - i2c_reg_write_byte(d->bus, d->addr, d->h12_reg, - v & (uint8_t)~d->h12_mask) == 0; + if (!(v & d->h12_mask)) { + return true; + } + w = v & (uint8_t)~d->h12_mask; + for (int tries = 0; tries < 2; tries++) { + if (i2c_reg_write_byte(d->bus, d->addr, d->h12_reg, w) != 0) { + return false; + } + if (i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &r) == 0 && r == w) { + return true; + } + } + return false; } #if RTC_RV3028_CFG diff --git a/zephcore/boards/common/rtc-i2c.dtsi b/zephcore/boards/common/rtc-i2c.dtsi index 46614cd58..a9fc67a8a 100644 --- a/zephcore/boards/common/rtc-i2c.dtsi +++ b/zephcore/boards/common/rtc-i2c.dtsi @@ -39,8 +39,8 @@ rtc_ds3231: rtc-ds3231@68 { date-index = <4>; status-reg = <0x0F>; /* control/status */ status-mask = <0x80>; /* OSF — oscillator stopped */ - /* Hours bit 6, 12-hour mode: DS3231 (p. 12), DS1307 (p. 8). In the - * time block, so the zero byte is unused. */ + /* Hours bit 6, 12-hour mode: DS3231 (p. 12), DS1307 (p. 8). Hours + * bit 7 is left to zero-mask below. */ twelve-hour-bit = [02 40 00]; /* Bits shown as 0 in all three parts: DS3231 (19-5170 Rev 10, Figure 1, * p. 11) and DS3232 (19-5337 Rev 5, Figure 1, p. 11), where 05h bit 7 is From c18e1f05d2e217cf0e044502401d144d84abb516 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 15:40:12 -0700 Subject: [PATCH 10/19] rtc: promise a sync only for the RTC it will write CodeRabbit (PR 52): a boot log for an RTC that is present but not the adopted write-back target said "clock will be set on the next GPS/app/CLI sync", which zephcore_rtc_save() never does for it. The clause is now added only for the adopted chip. Swept the three sibling log lines in the same loop (time unreadable twice, power-loss flag), which made the same promise. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 3285d4d7f..efd209073 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -691,26 +691,26 @@ static bool rtc_probe(uint32_t *epoch_out) } #endif } + /* Only the write-back target is set by a later sync. */ + const char *then = d == s_active ? + " — clock will be set on the next GPS/app/CLI sync" : ""; if (v == RTC_FOUND_GARBLED) { - LOG_WRN("%s present, time unreadable — clock will be set on the " - "next GPS/app/CLI sync", d->name); + LOG_WRN("%s present, time unreadable%s", d->name, then); continue; } int h12 = rtc_12h(d, blk); if (h12 != 0) { - LOG_WRN("%s present, %s — clock will be set on the next " - "GPS/app/CLI sync", d->name, - h12 > 0 ? "12-hour mode" : "hour mode unreadable"); + LOG_WRN("%s present, %s%s", d->name, + h12 > 0 ? "12-hour mode" : "hour mode unreadable", then); continue; } int flag = rtc_power_flag(d, blk); if (flag != 0) { - LOG_WRN("%s present, power-loss flag %s — clock will be set " - "on the next GPS/app/CLI sync", d->name, - flag > 0 ? "set" : "unreadable"); + LOG_WRN("%s present, power-loss flag %s%s", d->name, + flag > 0 ? "set" : "unreadable", then); continue; } @@ -720,8 +720,7 @@ static bool rtc_probe(uint32_t *epoch_out) /* 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)) { - LOG_WRN("%s present, time unreadable — clock will be set on the " - "next GPS/app/CLI sync", d->name); + LOG_WRN("%s present, time unreadable%s", d->name, then); continue; } From 2bde4209f98e411fd635ab47372bf43e2a31173b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 7 Oct 2026 07:06:11 -0700 Subject: [PATCH 11/19] rtc: clear the RV3028 12-hour bit once at boot instead, as the maintainer asked liquidraver/ZephCore#103 review (comment 6037017924), on #104: take what MeshCore does. Drop the write-path 24-hour select, its read-back retry, the 12-hour read refusal, the zero bits and the RTC_H12_CHECK build asserts, and the DS3231/DS1307 node property. twelve-hour-bit is now [register mask], set on the two RV3028 nodes. Once the chip is identified at boot, rtc_probe() clears a set bit, never on an FFh read, and re-reads the time block; the chip converts its own hours. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 152 +++++------------- zephcore/adapters/clock/ZephyrRTCDiscover.h | 16 +- zephcore/boards/common/rtc-i2c.dtsi | 5 +- .../boards/nrf52840/rak4631/board.overlay | 2 +- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 28 ++-- 5 files changed, 61 insertions(+), 142 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index efd209073..503148467 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -40,9 +40,8 @@ struct rtc_desc { uint8_t status_mask; /* "time unreliable" bit within status_reg */ const uint8_t *zero; /* 7 bytes of bits the data sheet shows as 0, or NULL */ bool week_one_hot; /* weekday as one bit per day, not 0-6 */ - uint8_t h12_reg; /* register of the 12-hour mode bit */ - uint8_t h12_mask; /* that bit, or 0 if the chip has no 12-hour mode */ - uint8_t h12_zero; /* bits of h12_reg that always read 0 */ + uint8_t h12_reg; /* register of the 12-hour bit */ + uint8_t h12_mask; /* 12-hour bit, or 0 if none */ #if RTC_RV3028_CFG const uint8_t *cfg; /* rv3028-eeprom-config triplets, or NULL */ uint8_t cfg_len; @@ -61,27 +60,6 @@ struct rtc_desc { DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_ZERO_ARRAY) -#define RTC_H12_REG(node) DT_PROP_BY_IDX(node, twelve_hour_bit, 0) -#define RTC_H12_CHECK(node) \ - IF_ENABLED(DT_NODE_HAS_PROP(node, twelve_hour_bit), \ - (BUILD_ASSERT(DT_PROP_LEN(node, twelve_hour_bit) == 3, \ - "twelve-hour-bit is [register mask zero]"); \ - BUILD_ASSERT(DT_PROP_BY_IDX(node, twelve_hour_bit, 1) != 0, \ - "twelve-hour-bit needs a mode bit"); \ - BUILD_ASSERT(!(RTC_H12_REG(node) >= DT_PROP(node, time_reg) && \ - RTC_H12_REG(node) < DT_PROP(node, time_reg) + 7) || \ - RTC_H12_REG(node) == DT_PROP(node, time_reg) + 2, \ - "twelve-hour-bit in the time block must be in hours"); \ - BUILD_ASSERT((RTC_H12_REG(node) >= DT_PROP(node, time_reg) && \ - RTC_H12_REG(node) < DT_PROP(node, time_reg) + 7) || \ - (DT_PROP_BY_IDX(node, twelve_hour_bit, 2) & 0x01), \ - "twelve-hour-bit outside the time block needs bit 0 as a zero bit"); \ - BUILD_ASSERT((DT_PROP_BY_IDX(node, twelve_hour_bit, 1) & \ - DT_PROP_BY_IDX(node, twelve_hour_bit, 2)) == 0, \ - "twelve-hour-bit zero bits overlap the mode bit");)) - -DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_H12_CHECK) - #if RTC_RV3028_CFG #define RTC_CFG_NAME(node) _CONCAT(rtc_cfg_, DT_DEP_ORD(node)) @@ -103,6 +81,10 @@ DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_CFG_ARRAY) #define RTC_CFG_FIELDS(node) #endif +#define RTC_H12(node, i) \ + COND_CODE_1(DT_NODE_HAS_PROP(node, twelve_hour_bit), \ + (DT_PROP_BY_IDX(node, twelve_hour_bit, i)), (0)) + #define RTC_DESC_ENTRY(node) \ { \ .bus = DEVICE_DT_GET(DT_BUS(node)), \ @@ -115,15 +97,8 @@ DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_CFG_ARRAY) DT_NODE_HAS_PROP(node, zero_mask), \ (RTC_ZERO_NAME(node)), (NULL)), \ .week_one_hot = DT_PROP(node, weekday_one_hot), \ - .h12_reg = COND_CODE_1( \ - DT_NODE_HAS_PROP(node, twelve_hour_bit), \ - (DT_PROP_BY_IDX(node, twelve_hour_bit, 0)), (0)), \ - .h12_mask = COND_CODE_1( \ - DT_NODE_HAS_PROP(node, twelve_hour_bit), \ - (DT_PROP_BY_IDX(node, twelve_hour_bit, 1)), (0)), \ - .h12_zero = COND_CODE_1( \ - DT_NODE_HAS_PROP(node, twelve_hour_bit), \ - (DT_PROP_BY_IDX(node, twelve_hour_bit, 2)), (0)), \ + .h12_reg = (uint8_t)RTC_H12(node, 0), \ + .h12_mask = (uint8_t)RTC_H12(node, 1), \ RTC_CFG_FIELDS(node) \ .name = DT_NODE_FULL_NAME(node), \ }, @@ -257,6 +232,22 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) return RTC_FOUND; } +/* Clear a set 12-hour bit; the chip converts its hours itself. Never on an + * FFh read. True if cleared. */ +static bool rtc_clear_12h(const struct rtc_desc *d) +{ + uint8_t r; + + if (d->h12_mask == 0 || + i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &r) != 0 || + r == 0xFF || !(r & d->h12_mask) || + i2c_reg_write_byte(d->bus, d->addr, d->h12_reg, r & ~d->h12_mask) != 0) { + return false; + } + LOG_INF("%s: 12-hour mode cleared", d->name); + return true; +} + /* 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]) { @@ -292,63 +283,6 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) } } -/* Read an out-of-block 12-hour mode register. False if the read failed or - * shows a bit that always reads 0, as a read cut short (ending in 1s) does. */ -static bool rtc_read_h12_reg(const struct rtc_desc *d, uint8_t *v) -{ - return i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, v) == 0 && - !(*v & d->h12_zero); -} - -/* 1 in 12-hour mode, 0 in 24-hour mode or with none, -1 if unreadable. */ -static int rtc_12h(const struct rtc_desc *d, const uint8_t blk[7]) -{ - uint8_t v; - - if (d->h12_mask == 0) { - return 0; - } - if (d->h12_reg >= d->time_reg && d->h12_reg < d->time_reg + 7) { - v = blk[d->h12_reg - d->time_reg]; - } else if (!rtc_read_h12_reg(d, &v)) { - return -1; - } - if (v & d->h12_zero) { - return -1; - } - return (v & d->h12_mask) != 0; -} - -/* Select 24-hour mode: clear an out-of-block bit (the RV3028 converts its - * Hours itself, 02h); an in-block one (DS3231 hours bit 6) is cleared by the - * time write's hours byte. The clear is read back and written once more if - * it differs, which repairs a write torn by a switchover (5.10). */ -static bool rtc_select_24h(const struct rtc_desc *d) -{ - uint8_t v, w, r; - - if (d->h12_mask == 0 || - (d->h12_reg >= d->time_reg && d->h12_reg < d->time_reg + 7)) { - return true; - } - if (!rtc_read_h12_reg(d, &v)) { - return false; - } - if (!(v & d->h12_mask)) { - return true; - } - w = v & (uint8_t)~d->h12_mask; - for (int tries = 0; tries < 2; tries++) { - if (i2c_reg_write_byte(d->bus, d->addr, d->h12_reg, w) != 0) { - return false; - } - if (i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &r) == 0 && r == w) { - return true; - } - } - return false; -} - #if RTC_RV3028_CFG /* RV3028 registers for reading and writing its configuration EEPROM. */ @@ -367,14 +301,12 @@ static bool rtc_select_24h(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 before the other time bytes and the real year last (24-hour mode - * is selected before both): 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. */ +/* 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 rtc_select_24h(d) && - i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00) == 0 && + 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; } @@ -668,6 +600,10 @@ static bool rtc_probe(uint32_t *epoch_out) if (v == RTC_ABSENT) { continue; } + if (rtc_clear_12h(d) && v == RTC_FOUND && + i2c_burst_read(d->bus, d->addr, d->time_reg, blk, 7) != 0) { + v = RTC_FOUND_GARBLED; + } #if RTC_RV3028_CFG /* Only a clean identification is configured: the store writes to @@ -691,26 +627,18 @@ static bool rtc_probe(uint32_t *epoch_out) } #endif } - /* Only the write-back target is set by a later sync. */ - const char *then = d == s_active ? - " — clock will be set on the next GPS/app/CLI sync" : ""; if (v == RTC_FOUND_GARBLED) { - LOG_WRN("%s present, time unreadable%s", d->name, then); - continue; - } - int h12 = rtc_12h(d, blk); - - if (h12 != 0) { - LOG_WRN("%s present, %s%s", d->name, - h12 > 0 ? "12-hour mode" : "hour mode unreadable", then); + LOG_WRN("%s present, time unreadable — clock will be set on the " + "next GPS/app/CLI sync", d->name); continue; } int flag = rtc_power_flag(d, blk); if (flag != 0) { - LOG_WRN("%s present, power-loss flag %s%s", d->name, - flag > 0 ? "set" : "unreadable", then); + LOG_WRN("%s present, power-loss flag %s — clock will be set " + "on the next GPS/app/CLI sync", d->name, + flag > 0 ? "set" : "unreadable"); continue; } @@ -720,7 +648,8 @@ static bool rtc_probe(uint32_t *epoch_out) /* 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)) { - LOG_WRN("%s present, time unreadable%s", d->name, then); + LOG_WRN("%s present, time unreadable — clock will be set on the " + "next GPS/app/CLI sync", d->name); continue; } @@ -789,8 +718,7 @@ void zephcore_rtc_save(uint32_t epoch) } } else #endif - if (!rtc_select_24h(d) || - 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 77e3cf242..bcf07abd4 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -27,9 +27,9 @@ extern "C" { * Probe the declared RTC chips in order, stopping at the first that holds a * sane time (year >= 2025 and its power-loss flag clear): store its Unix * epoch in *epoch_out and return true. The first chip found (valid time or - * not) is remembered as the write-back target. No time is taken while the - * descriptor's twelve-hour-bit reads set or cannot be read cleanly. Returns - * false if none is present or none holds a trustworthy time. + * not) is remembered as the write-back target. Returns false if none is + * present or none holds a trustworthy time. A set twelve-hour-bit is cleared + * before the time is read. * * 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. @@ -37,11 +37,11 @@ extern "C" { bool zephcore_rtc_restore(uint32_t *epoch_out); /* - * Persist an authoritative epoch to the discovered RTC chip, selecting - * 24-hour mode first, and clear its 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. + * Persist an authoritative epoch to the discovered RTC chip and clear its + * 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. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/boards/common/rtc-i2c.dtsi b/zephcore/boards/common/rtc-i2c.dtsi index a9fc67a8a..18d97bf32 100644 --- a/zephcore/boards/common/rtc-i2c.dtsi +++ b/zephcore/boards/common/rtc-i2c.dtsi @@ -39,9 +39,6 @@ rtc_ds3231: rtc-ds3231@68 { date-index = <4>; status-reg = <0x0F>; /* control/status */ status-mask = <0x80>; /* OSF — oscillator stopped */ - /* Hours bit 6, 12-hour mode: DS3231 (p. 12), DS1307 (p. 8). Hours - * bit 7 is left to zero-mask below. */ - twelve-hour-bit = [02 40 00]; /* Bits shown as 0 in all three parts: DS3231 (19-5170 Rev 10, Figure 1, * p. 11) and DS3232 (19-5337 Rev 5, Figure 1, p. 11), where 05h bit 7 is * Century, and DS1307 (Rev 3/15, Table 2, p. 8, "always reads back as @@ -68,7 +65,7 @@ rtc_rv3028: rtc-rv3028@52 { date-index = <4>; status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ - twelve-hour-bit = [10 02 01]; /* Control 2 12_24; RESET reads 0, p. 24 */ + twelve-hour-bit = [10 02]; /* Control 2 12_24, p. 24 */ /* RV-3028-C7 Application Manual Rev. 1.4, 3.2, p. 12 */ zero-mask = [80 80 c0 f8 c0 e0 00]; }; diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index 01a54d5c9..450f41f23 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -228,7 +228,7 @@ date-index = <4>; status-reg = <0x0e>; /* status */ status-mask = <0x01>; /* PORF — power-on reset */ - twelve-hour-bit = [10 02 01]; /* Control 2 12_24; RESET reads 0, p. 24 */ + twelve-hour-bit = [10 02]; /* Control 2 12_24, p. 24 */ /* RV-3028-C7 Application Manual Rev. 1.4, 3.2, p. 12 */ zero-mask = [80 80 c0 f8 c0 e0 00]; /* The RAK12002's backup is a supercapacitor. As the RV-3028-C7 diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 46019325e..0f2a045fe 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -65,17 +65,11 @@ properties: twelve-hour-bit: type: uint8-array description: > - [register mask zero]: the bit that reads set while the chip counts - hours in 12-hour mode, and the bits of that register that always - read 0, which must not include the mode bit. Leave it out for a - chip with no 12-hour mode. Inside the time block it must be the - hours register. Outside it, the zero bits must include bit 0 (the - build checks these): a read cut short ends in 1s, so it always - shows bit 0 set, and such a read is never written back. While the - mode bit reads set, or cannot be read cleanly, no time is taken - from the chip. Each time write selects 24-hour mode first: a bit - outside the time block is cleared, and one in hours is cleared by - the write's own hours byte. + [register mask]: a bit outside the time block that reads set in + 12-hour mode, where clearing it makes the chip convert its own + hours to 24-hour, such as the RV3028's Control 2 12_24. At boot, + once the chip is identified, a set bit is cleared before the time + is read, unless the register reads FFh. rv3028-eeprom-config: type: uint8-array @@ -97,9 +91,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 before the - other time bytes 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. + 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 9b68b7e1ae51866baab2690695af9865909dd94a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 7 Oct 2026 07:06:20 -0700 Subject: [PATCH 12/19] rtc: write the RTC time only for years 2000-2099 ZephCore writes the year as two BCD digits with no century, as y % 100. A time before 2000, accepted by a node whose clock had no time yet, was stored as a year near the end of the century and restored at the next boot above the 2025 floor: `time 900000000` (1998) came back as 2098. rtc_time_block() now fails outside 2000-2099, and zephcore_rtc_save() then logs and writes no time, for every chip; the chip keeps the time it has. An RV3028 write retry stops if its run-on time leaves the range. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/Kconfig | 5 +++-- zephcore/adapters/clock/ZephyrRTCDiscover.c | 21 ++++++++++++++----- zephcore/adapters/clock/ZephyrRTCDiscover.h | 6 +++--- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 14 ++++++------- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 43430a802..b9365f151 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 (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/Kconfig b/zephcore/Kconfig index d80b0b14f..dd5d4d282 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -911,8 +911,9 @@ 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 b701a4b5b..d439811df 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -224,8 +224,9 @@ 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 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; unsigned m, day; @@ -236,6 +237,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); @@ -244,6 +248,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; @@ -537,8 +542,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)) { + LOG_WRN("RTC %s: time outside 2000-2099 not written", s_active->name); + return; + } if (!rv3028_steady(s_active, blk, true)) { if (++s_save_tries < RTC_SAVE_RETRIES) { k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); @@ -675,7 +683,10 @@ 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)) { + 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 42943c3a7..7b745ec07 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 @@ -40,7 +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. + * 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 efde76778..505784c50 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. From 069c974cc0b20bbf405cef036e5e2c7f85f8682f Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:55:14 +0200 Subject: [PATCH 13/19] UI fixes --- releasenotes/RELEASE_NOTES_1.17.7-zephcore.md | 4 ++++ zephcore/helpers/ui-joystick/joystick_ui_task.cpp | 12 ++++++++++-- zephcore/helpers/ui-joystick/joystick_ui_task.h | 1 + zephcore/helpers/ui-joystick/screens/system.cpp | 1 - 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md b/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md index 62f0d0016..0b24b30d2 100644 --- a/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md +++ b/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md @@ -167,6 +167,10 @@ matches the SoftDevice version of your bootloader. measurement the chip rejected the command that puts it back into its duty cycle. The next channel check recovered it and no packets were lost; the driver now returns the chip to standby first, and the command is accepted. +- **Joystick UI** (Wio Tracker L1 and other joystick boards): switching the GPS on from the GPS + screen showed "GPS disabled", and switching it off showed "GPS enabled". The GPS itself switched + correctly; the message now matches. The 3-tap buzzer and 4-tap GPS shortcuts now show a message + too, like the LED and advert shortcuts. - **New board**: Seeed LR2021 LoRa Plus EVK with a XIAO nRF54LM20A. Build it from source; there is no published firmware for it yet. - **Zephyr** updated to `74b7173e9c9`. One change in it would have been visible and is handled: diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index 2d9827213..4b119bc3a 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -878,6 +878,7 @@ void JoystickUITask::loop() bool consumed = false; if (key == KEY_BUZZ_TOGGLE) { toggleBuzzer(); + showAlert(isBuzzerQuiet() ? "Buzzer off" : "Buzzer on", 1000); consumed = true; } else if (key == KEY_GPS_TOGGLE) { toggleGPS(); @@ -995,8 +996,15 @@ bool JoystickUITask::getGPSState() const void JoystickUITask::toggleGPS() { - if (!gps_is_available()) return; - mesh_gps_set_enabled(!gps_is_enabled()); + if (!gps_is_available()) { + showAlert("No GPS", 1000); + return; + } + /* The mesh loop applies the change later, so getGPSState() is still + * the old state here: the alert names the requested one. */ + bool enable = !gps_is_enabled(); + mesh_gps_set_enabled(enable); + showAlert(enable ? "GPS enabled" : "GPS disabled", 1000); } uint32_t JoystickUITask::getGpsDutySec() const diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.h b/zephcore/helpers/ui-joystick/joystick_ui_task.h index 226ea5a06..7449702f0 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.h +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.h @@ -114,6 +114,7 @@ class JoystickUITask { void toggleBuzzer(); bool isGPSAvailable() const; bool getGPSState() const; + /* Requests the opposite state and shows the alert for it. */ void toggleGPS(); /* GPS duty-cycle interval, seconds (0 = always on). Steps through a fixed * preset ladder (step = +1/-1) rather than raw seconds, since the valid diff --git a/zephcore/helpers/ui-joystick/screens/system.cpp b/zephcore/helpers/ui-joystick/screens/system.cpp index c8edcec2a..f8c1c5a5d 100644 --- a/zephcore/helpers/ui-joystick/screens/system.cpp +++ b/zephcore/helpers/ui-joystick/screens/system.cpp @@ -874,7 +874,6 @@ bool GPSSettingsScreen::handleInput(char key) if (key == KEY_ENTER) { if (_selected == 0) { _task->toggleGPS(); - _task->showAlert(_task->getGPSState() ? "GPS enabled" : "GPS disabled", 1000); } else { _task->adjustGpsDuty(1); } From ee08b2171c61da30ba07da65b22b4ccc994c64e1 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 7 Oct 2026 09:09:26 -0700 Subject: [PATCH 14/19] rtc: clear the 12-hour bit only on a clean identification liquidraver/ZephCore#106 review (owner and Copilot): RTC_FOUND_GARBLED means the first read ruled the device out and the confirming read failed, so a write to 10h could reach a device that isn't the RTC. Test v == RTC_FOUND first, as the EEPROM config path does. Also one clause in the ARCHITECTURE.md Clock paragraph for the boot-time clear. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/adapters/clock/ZephyrRTCDiscover.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 43430a802..563d412bc 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 (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, and an RV3028 found in 12-hour mode has its `twelve-hour-bit` cleared first, so the chip converts its own hours. 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 503148467..afc1e7bf8 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -600,7 +600,7 @@ static bool rtc_probe(uint32_t *epoch_out) if (v == RTC_ABSENT) { continue; } - if (rtc_clear_12h(d) && v == RTC_FOUND && + if (v == RTC_FOUND && rtc_clear_12h(d) && i2c_burst_read(d->bus, d->addr, d->time_reg, blk, 7) != 0) { v = RTC_FOUND_GARBLED; } From 3c101387140bf38b9c12dae992be6cc402cc7a18 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 7 Oct 2026 09:17:20 -0700 Subject: [PATCH 15/19] rtc: say the 12-hour clear needs a clean identification Local review: the binding, the header and ARCHITECTURE.md still said the bit is cleared once the chip is identified, which in this file includes RTC_FOUND_GARBLED. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/adapters/clock/ZephyrRTCDiscover.h | 4 ++-- zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 563d412bc..b54038c35 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, and an RV3028 found in 12-hour mode has its `twelve-hour-bit` cleared first, so the chip converts its own hours. 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), 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, and an RV3028 cleanly identified in 12-hour mode has its `twelve-hour-bit` cleared first, so the chip converts its own hours. 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.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index bcf07abd4..112f5269d 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -28,8 +28,8 @@ extern "C" { * sane time (year >= 2025 and its power-loss flag clear): store its Unix * epoch in *epoch_out and return true. The first chip found (valid time or * not) is remembered as the write-back target. Returns false if none is - * present or none holds a trustworthy time. A set twelve-hour-bit is cleared - * before the time is read. + * present or none holds a trustworthy time. On a clean identification, a set + * twelve-hour-bit is cleared before the time is taken. * * 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. diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 0f2a045fe..1d37ced43 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -67,9 +67,9 @@ properties: description: > [register mask]: a bit outside the time block that reads set in 12-hour mode, where clearing it makes the chip convert its own - hours to 24-hour, such as the RV3028's Control 2 12_24. At boot, - once the chip is identified, a set bit is cleared before the time - is read, unless the register reads FFh. + hours to 24-hour, such as the RV3028's Control 2 12_24. Once the + chip is cleanly identified, a set bit is cleared before a time is + taken from it, unless the register reads FFh. rv3028-eeprom-config: type: uint8-array From 290b0c26b04adaef3f1386dbf8f5a85fb58b3728 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:06:39 +0200 Subject: [PATCH 16/19] guidelines for Giskard Reventlov vol.1 --- AGENTS.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..3f4b69365 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,65 @@ +# AGENTS.md + +Rules for AI-assisted contributions to ZephCore. They apply to human contributors too. Read the section of +`docs/DESIGN.md` (invariants, layer rules) and `docs/ARCHITECTURE.md` (component reference) for the area you +touch before writing code. Decisions are in `docs/adr/`. + +## How a change is judged + +1. **Find the root cause.** The PR states the cause and the evidence for it (datasheet section, register dump, + log, trace). If the cause is not known, say so; do not ship a guess. +2. **No band-aids.** Do not add a retry, delay, timeout, re-init, read-back check or watchdog to make a symptom + go away. Recovery code is accepted only for a named, understood hardware failure, in the layer that owns the + state, with its bound (`docs/DESIGN.md` §8.7). There is no hardware watchdog and none will be added. +3. **The smallest change that fixes it.** Close what is reachable today, not what might be reachable later. No + speculative guards, build asserts, new abstractions, options or refactors riding along. One concern per PR. + If the diff is larger than the bug, expect to be asked to cut it. +4. **Do what upstream MeshCore does** when it already solves the problem, unless ours can be more accurate at no + compatibility cost (`docs/DESIGN.md` §9). +5. **Comment diet.** A comment says what the code does or the invariant it keeps, in a few lines. Rationale, + history, incidents, measurements and rejected alternatives go in the PR description and `docs/`, not in code. + Do not comment the obvious. +6. **Fix it for every board.** A fix or feature that concerns behaviour goes in the common path, for all + platforms and roles. Do not scope it to the board or SoC family where it was found or tested, and do not + exclude a platform because the problem cannot happen there today; a later port or Zephyr bump can make it + real. Untested boards still get the change; the PR says which hardware it ran on. +7. **Gate on the capability, never on the name.** The only reason to gate code is that the hardware is absent, + and the test for that is a devicetree node or property, or a Kconfig symbol for the feature. Never + `CONFIG_SOC_*`, `CONFIG_BOARD_*` or a board name as a stand-in for "has this part" or "has this bug". A board + that lacks the hardware then carries no code for it, and a new board that has it gets the behaviour for free. +8. **A real platform difference is stated as one.** If something truly is specific to one SoC or chip (an + erratum, a vendor blob, a peripheral that exists nowhere else), it lives in that platform's layer and the PR + cites the source that makes it specific. "Only seen on X" is not that. + +## Invariants: do not change without an ADR + +- Byte compatibility with Arduino MeshCore on the air and on the companion protocol; `prefs.json` format. +- The wall clock only moves forward. +- Mesh, GPS state and UI run on the main thread; loops are event-driven, no polling. +- No heap in the packet path. Companion builds are RAM-bound. +- Never write to a peripheral that was not cleanly identified first. +- BLE: no proactive SMP Security Request, keep `BT_LE_ADV_OPT_USE_IDENTITY` (ADR 0004), leave connection + parameters alone. +- Files ported from MeshCore (listed in `.editorconfig`) keep upstream's text verbatim; every change in them is + fenced `// ZEPHCORE:`. Do not restyle or de-duplicate them. +- `zephcore/patches/zephyr`: one patch owns each Zephyr file. +- `board.conf` does not repeat what a parent conf or the devicetree already provides. + +## Mechanics + +- Branch from `dev`, target `dev`, rebase instead of merging. +- Tabs for indentation, except the upstream-ported files. +- Build with `--pristine` when the board or role changes; ESP32 S3/C-series repeaters need `--sysbuild`. +- Host tests: `python3 tests/run.py run --profile quick` (Linux or WSL). A new case needs a + `tests/catalog.json` entry. +- New board: `zephcore.yml` without `release:`, then `python zephcore/scripts/board_manifest.py check` and + `... docs`. +- Docs in the same PR: a clause in `docs/ARCHITECTURE.md` when behaviour, an interface or a limit changes; + `docs/CLI_commands.md` for any CLI change; an ADR when a decision is taken or reversed. + +## PR description + +- Cause, fix, and what was deliberately left out. +- Which boards it was built for, and which hardware it actually ran on. Never claim hardware verification that + was not done. +- Flash and RAM delta when code is added to a common path. From 0c82ea19d266fec7245a8bfe444b71e5155599f4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 8 Oct 2026 06:37:30 -0700 Subject: [PATCH 17/19] rtc: take no time when the 12-hour clear fails, as the maintainer asked If the 12_24 bit could not be read (or read FFh) or the clear or the re-read failed, the block was still decoded as 24-hour and could restore hours 8 or 12 h ahead. That case is now RTC_FOUND_GARBLED. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/adapters/clock/ZephyrRTCDiscover.c | 24 ++++++++++++------- zephcore/adapters/clock/ZephyrRTCDiscover.h | 3 ++- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 3 ++- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4b9f3c4cc..1b9a9a90b 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), 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, and an RV3028 cleanly identified in 12-hour mode has its `twelve-hour-bit` cleared first, so the chip converts its own hours. 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, and an RV3028 cleanly identified in 12-hour mode has its `twelve-hour-bit` cleared first, so the chip converts its own hours (no time is taken if that bit cannot be read or cleared). 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 29173dd23..714a67efb 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -232,16 +232,23 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) return RTC_FOUND; } -/* Clear a set 12-hour bit; the chip converts its hours itself. Never on an - * FFh read. True if cleared. */ -static bool rtc_clear_12h(const struct rtc_desc *d) +/* Clear a set 12-hour bit and re-read blk; the chip converts its hours + * itself. False on a failed read, an FFh read or a failed clear. */ +static bool rtc_clear_12h(const struct rtc_desc *d, uint8_t blk[7]) { uint8_t r; - if (d->h12_mask == 0 || - i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &r) != 0 || - r == 0xFF || !(r & d->h12_mask) || - i2c_reg_write_byte(d->bus, d->addr, d->h12_reg, r & ~d->h12_mask) != 0) { + if (d->h12_mask == 0) { + return true; + } + if (i2c_reg_read_byte(d->bus, d->addr, d->h12_reg, &r) != 0 || r == 0xFF) { + return false; + } + if (!(r & d->h12_mask)) { + return true; + } + if (i2c_reg_write_byte(d->bus, d->addr, d->h12_reg, r & ~d->h12_mask) != 0 || + i2c_burst_read(d->bus, d->addr, d->time_reg, blk, 7) != 0) { return false; } LOG_INF("%s: 12-hour mode cleared", d->name); @@ -608,8 +615,7 @@ static bool rtc_probe(uint32_t *epoch_out) if (v == RTC_ABSENT) { continue; } - if (v == RTC_FOUND && rtc_clear_12h(d) && - i2c_burst_read(d->bus, d->addr, d->time_reg, blk, 7) != 0) { + if (v == RTC_FOUND && !rtc_clear_12h(d, blk)) { v = RTC_FOUND_GARBLED; } diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 336345685..5c36f6942 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -29,7 +29,8 @@ extern "C" { * epoch in *epoch_out and return true. The first chip found (valid time or * not) is remembered as the write-back target. Returns false if none is * present or none holds a trustworthy time. On a clean identification, a set - * twelve-hour-bit is cleared before the time is taken. + * twelve-hour-bit is cleared before the time is taken, and no time is taken + * if that bit cannot be read or cleared. * * 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. diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index e4e35a80f..877230c10 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -69,7 +69,8 @@ properties: 12-hour mode, where clearing it makes the chip convert its own hours to 24-hour, such as the RV3028's Control 2 12_24. Once the chip is cleanly identified, a set bit is cleared before a time is - taken from it, unless the register reads FFh. + taken from it. No time is taken if the register cannot be read, + reads FFh, or does not take the clear. rv3028-eeprom-config: type: uint8-array From 5396538cfa0d11e8b9c5963d211c393e017578d8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 8 Oct 2026 07:15:38 -0700 Subject: [PATCH 18/19] rtc: name the re-read failure in the 12-hour clear's text Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/adapters/clock/ZephyrRTCDiscover.c | 3 ++- zephcore/adapters/clock/ZephyrRTCDiscover.h | 2 +- zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1b9a9a90b..7f528252c 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), 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, and an RV3028 cleanly identified in 12-hour mode has its `twelve-hour-bit` cleared first, so the chip converts its own hours (no time is taken if that bit cannot be read or cleared). 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. An RV-3028 cleanly identified in 12-hour mode has its `twelve-hour-bit` cleared first, so the chip converts its own hours, and no time is taken if that register cannot be read or the clear or re-read fails. 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 714a67efb..6de57586d 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -233,7 +233,8 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) } /* Clear a set 12-hour bit and re-read blk; the chip converts its hours - * itself. False on a failed read, an FFh read or a failed clear. */ + * itself. False on a failed or FFh read of the bit, or a failed clear or + * re-read. */ static bool rtc_clear_12h(const struct rtc_desc *d, uint8_t blk[7]) { uint8_t r; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 5c36f6942..f22af0471 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -30,7 +30,7 @@ extern "C" { * not) is remembered as the write-back target. Returns false if none is * present or none holds a trustworthy time. On a clean identification, a set * twelve-hour-bit is cleared before the time is taken, and no time is taken - * if that bit cannot be read or cleared. + * if that read, the clear or the re-read fails. * * 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. diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 877230c10..16fa8b4e9 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -69,8 +69,8 @@ properties: 12-hour mode, where clearing it makes the chip convert its own hours to 24-hour, such as the RV3028's Control 2 12_24. Once the chip is cleanly identified, a set bit is cleared before a time is - taken from it. No time is taken if the register cannot be read, - reads FFh, or does not take the clear. + taken from it. No time is taken if the register cannot be read or + reads FFh, or if the write or the re-read fails. rv3028-eeprom-config: type: uint8-array From e5623aa5dddef20184aee957223433f9f8fa3ad7 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 8 Oct 2026 07:19:25 -0700 Subject: [PATCH 19/19] rtc: scope the no-time rule to every clean RV-3028, note the store delay Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/adapters/clock/ZephyrRTCDiscover.h | 4 ++-- zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7f528252c..e79158651 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), 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. An RV-3028 cleanly identified in 12-hour mode has its `twelve-hour-bit` cleared first, so the chip converts its own hours, and no time is taken if that register cannot be read or the clear or re-read fails. 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. An RV-3028 cleanly identified in 12-hour mode has its `twelve-hour-bit` cleared first, so the chip converts its own hours. No time is taken from a cleanly identified RV-3028 whose `twelve-hour-bit` register cannot be read or reads FFh, or whose clear or re-read fails. 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.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index f22af0471..4af5b28ef 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -29,8 +29,8 @@ extern "C" { * epoch in *epoch_out and return true. The first chip found (valid time or * not) is remembered as the write-back target. Returns false if none is * present or none holds a trustworthy time. On a clean identification, a set - * twelve-hour-bit is cleared before the time is taken, and no time is taken - * if that read, the clear or the re-read fails. + * twelve-hour-bit is cleared before the time is taken. No time is taken if its + * register cannot be read or reads FFh, or if the clear or the re-read fails. * * 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. diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 16fa8b4e9..bcd205459 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -90,7 +90,8 @@ properties: 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 + identify it cleanly, or whose 12-hour clear failed, waits for those + retries. The time is read at 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