From db69172c66cea391efda4c24b8dc00ce95acad9c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 8 Oct 2026 06:37:37 -0700 Subject: [PATCH] rtc: clear the RV3028 12-hour bit at boot ZephCore assumed 24-hour mode. If other firmware had left an RV3028 in 12-hour mode, the boot read masked the hours with 0x3F and could restore a time up to twelve hours off, and each save's 24-hour hours byte meant a different hour to the chip. A new optional twelve-hour-bit = [register mask] names the bit, set on the two RV3028 nodes to Control 2 12_24. Once the chip is cleanly identified at boot, rtc_probe() clears a set bit and re-reads the time block; the chip converts its own hours, as MeshCore relies on. If the register cannot be read, reads FFh, or the clear or re-read fails, no time is taken from the chip. DS3231/DS1307 are unchanged: a save's hours byte already selects 24-hour mode there. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/adapters/clock/ZephyrRTCDiscover.c | 35 +++++++++++++++++++ zephcore/adapters/clock/ZephyrRTCDiscover.h | 4 ++- zephcore/boards/common/rtc-i2c.dtsi | 1 + .../boards/nrf52840/rak4631/board.overlay | 1 + .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 13 ++++++- 6 files changed, 53 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b9365f1..e791586 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. 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.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index d439811..6de5758 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 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; @@ -79,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)), \ @@ -91,6 +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 = (uint8_t)RTC_H12(node, 0), \ + .h12_mask = (uint8_t)RTC_H12(node, 1), \ RTC_CFG_FIELDS(node) \ .name = DT_NODE_FULL_NAME(node), \ }, @@ -224,6 +232,30 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) return RTC_FOUND; } +/* Clear a set 12-hour bit and re-read blk; the chip converts its hours + * 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; + + 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); + return true; +} + /* 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]) @@ -584,6 +616,9 @@ static bool rtc_probe(uint32_t *epoch_out) if (v == RTC_ABSENT) { continue; } + if (v == RTC_FOUND && !rtc_clear_12h(d, blk)) { + v = RTC_FOUND_GARBLED; + } #if RTC_RV3028_CFG /* Only a clean identification is configured: the store writes to diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 7b745ec..4af5b28 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -28,7 +28,9 @@ 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. + * present or none holds a trustworthy time. On a clean identification, a set + * 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/boards/common/rtc-i2c.dtsi b/zephcore/boards/common/rtc-i2c.dtsi index 2dabd13..18d97bf 100644 --- a/zephcore/boards/common/rtc-i2c.dtsi +++ b/zephcore/boards/common/rtc-i2c.dtsi @@ -65,6 +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]; /* 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 02ef9ed..450f41f 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 = [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 505784c..bcd2054 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: uint8-array + 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. 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 or + reads FFh, or if the write or the re-read fails. + rv3028-eeprom-config: type: uint8-array description: > @@ -80,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