Skip to content

rak4631: use a fitted RAK12002 RTC, and keep its backup switchover enabled - #98

Merged
liquidraver merged 1 commit into
liquidraver:devfrom
ptr727:rak4631-rak12002-rtc
Oct 5, 2026
Merged

liquidraver merged 1 commit into
liquidraver:devfrom
ptr727:rak4631-rak12002-rtc

Conversation

@ptr727

@ptr727 ptr727 commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Manual references are to the RV-3028-C7 Application Manual, Rev. 1.4, November 2021 (Micro Crystal, RV-3028-C7), cited as §section and page. Code links are permalinks:

The defect

Two separate failures, both on a RAK4631 with a RAK12002 fitted (an RV3028 at 0x52).

  1. The RTC is never used. The RAK4631 overlay declares no zephcore,rtc-i2c candidate, so rtc_probe() never sees the chip. The time survives a warm reboot only in noinit RAM, so every power cut starts the node at 1970 until the next GPS, app or CLI sync.
  2. Declaring it is not enough. The RV3028 ships with backup switchover disabled: EEPROM 37h has BSM = 00 on delivery (§3.15.6, p. 39; §3.18, p. 41). With the chip declared but left as delivered, a power cut drops VDD with nothing switching to the supercap, so the chip resets and the time is lost (Testing, row B).

The RAK12002's backup is a supercapacitor. §7.3 (p. 105) specifies direct switching (DSM) with the trickle charger for a capacitor backup, and adds that "Power Management settings have to be stored in EEPROM for permanent configuration". A RAM-only setting does not last. With EERD = 0, the chip reloads its configuration RAM from EEPROM at power-on and every day at midnight (§4.6.2, p. 54). §4.6.9 (p. 57) warns that "the new, changed configurations are lost as soon as a refresh occurs".

The fix

Declare the RV3028 in the RAK4631 overlay (board.overlay), with the descriptor and zero-mask that rtc-i2c.dtsi uses for it. The overlay declares only that candidate, because the file's DS3231 at 0x68 shares the address of WisBlock IMUs.

A new optional rv3028-eeprom-config property (binding) holds (register, mask, value) triplets for the configuration registers 35h-37h. The RAK4631 sets:

  • 35h: CLKOE = 0, so CLKOUT is off, since it is unused (§3.15.4, p. 37; §7.3 note 5, p. 105). Only that bit is masked; the chip's other 35h bits are kept.
  • 37h: TCE = 1, FEDE = 1, BSM = 01 (DSM), TCR = 00 (3 kΩ, the delivery value). FEDE "should always be set to 1" (§3.15.6, p. 39).
  • EEOffset[0] (37h bit 7) and BSIE keep the chip's own values, and 36h, the rest of the factory trim, is never written.

The overlay comment notes that any other RV3028 module at 0x52 gets the same settings, trickle charging included.

When the store runs. rtc_probe() calls the store only when two things hold:

A store skipped for either reason is retried from the 10-minute retry. Each retry (rv3028_cfg_retry_fn()) runs rtc_identify() again before any write, and stops if two reads rule the chip out.

How it stores the config. rv3028_store_config() follows §3.15.6 (p. 39), which says BSM must be 00 or 10 to read or write the EEPROM:

  1. Set EERD = 1, then wait for EEbusy = 0 (§4.6.7, p. 56). This also covers the ~66 ms power-on refresh. rv3028_eeprom_idle() ends the wait on the first failed status read, rather than polling a dead bus through each transfer's timeout.
  2. Save RAM 37h, then write it back with BSM = 00, so switchover is off while the EEPROM is accessed.
  3. For each configured byte, read the EEPROM copy with a single-byte read (EECMD 22h, §4.6.6, p. 55). Write it with a single-byte write (21h, §4.6.5, p. 55) only if its masked bits differ. The waits follow §4.6.7: 1 ms after a read or Refresh and 10 ms after a write, before polling EEbusy.
  4. Run a Refresh (12h, §4.6.4, p. 54). It reloads RAM from EEPROM and brings back the stored switchover mode. The config is then read back on every run.
  5. If the Refresh did not complete, write 37h as configured, in RAM only, once EEbusy reads 0. If EEbusy never clears, leave switchover off rather than enable it during an EEPROM operation. A retry or the daily refresh restores it.
  6. Clear EERD with a fresh read-modify-write of Control 1. Clearing it while EEbusy may still be set is a judgement call: §4.6.7 only describes it after EEbusy reads 0.

Failure handling (rv3028_configure()):

  • If the config is not stored, it is set in the RAM mirror once EEbusy reads 0. It then lasts until the chip's next refresh from EEPROM.
  • If the config is stored but EERD could not be cleared, that is logged separately, since EERD = 1 also stops the daily refresh.
  • Either way the store is retried every 10 minutes, at most 3 times per boot.

The time read and write (rv3028_steady(), rv3028_write_time()). A switchover to backup disables the chip's I2C interface mid-transfer (§4.2, p. 45). The rest of a read then comes back 0xFF, and a tail that starts inside the year byte can still be valid BCD: a year too high. So at a node with rv3028-eeprom-config, the time is read at boot and written on each sync with BSF cleared before (§3.7, p. 22) and checked after:

  • Read. A read that failed or saw BSF gives no time ("time unreadable") and skips that boot's store.
  • Write order. A write sets year 00h first, then 00h-05h, then the real year. The burst's seconds restart the prescaler (§4.5.1, p. 53).
  • A cut write. A power loss cutting the write after its first byte and before its last, which takes the MCU down so no retry can repeat it, leaves year 2000, so the next boot takes no time from it. With every other field valid it reads as "time not yet set" and still adopts the chip, because identity ignores the year.
  • Retry. A write that is not confirmed is repeated every 5 s, up to 12 times, with the time run on (rv3028_save_retry_fn()).

Gated. All of the RV3028 code is under #if DT_ANY_COMPAT_HAS_PROP_STATUS_OKAY(zephcore_rtc_i2c, rv3028_eeprom_config). A board without the property builds the same RTC object as dev.

An already-configured chip costs two EEPROM byte reads and a Refresh per boot, and no EEPROM write.

Notes for review

Testing

Builds at ad2df09: rak4631 repeater and companion, release (CONFIG_LOG=n) and debug, and thinknode_m1 companion. There are no compiler warnings.

  • On thinknode_m1, ZephyrRTCDiscover.c.obj is 1628 B of text, identical to dev, with no RV3028 symbols.
  • On the RAK4631 repeater the feature costs about 3 KB of text against dev.

Hardware: a RAK4631 with a RAK12002, USB only (no battery fitted), debug repeater builds. The time CLI command set the clock from the host before each check.

Power cut (USB unplugged and replugged by the maintainer; rows A-D are from the earlier revision, raw logs in this comment):

Row Build Cut Boot after the cut clock
A dev f9d01cf 69 s no RTC lines (no candidate declared) 1/1/1970: time lost
B declaration only, delivery EEPROM (BSM = 00) 88 s RTC at 2000-01-01; power-loss flag set 1/1/1970: time lost
D earlier revision of this PR, 334d9f7 65 s restored the time set before the cut time kept
E this rebase, b489046 (BSF-bracketed read and write) 66 s restored the time set before the cut, to the second time kept

This rebase, on the same board. The boot restore through the bracketed read and the bracketed save were checked after each revision. A warm reboot after the year-last write (3f51c58) restored exactly the written time. The board's EEPROM had already been configured by the earlier revision (35h/37h = 40/34), so these boots wrote nothing to it. The commits after 3f51c58 change no RTC code in debug builds:

  • b574fdf moves the RAM fallback, which runs only after a failed store, out of a log call. Release builds now run it.
  • The last commit is comments only. The RTC object is byte-identical.

EEPROM store (earlier revision, from delivery EEPROM; the store code is unchanged apart from the EEbusy gate on its failure paths):

Step EEPROM 35h / 37h read by the store RAM 35h / 37h after Log
First boot C0 / 10 40 / 34 config committed to EEPROM; switchover off 52.2 ms
Reboot 40 / 34 40 / 34 nothing written; switchover off 14.3 ms
RTC stepped past its midnight refresh — 40 / 34 before and after 00:00:00 config held, so it is in EEPROM, not RAM only

Not tested on hardware: paths that need fault injection, namely the RAM fallback, the EERD-only failure, the config and save retries, a switchover during the read or write, and a cut write leaving year 2000. Also untested: a battery-fitted board, and temperature.

Known limitations (none is reached on a healthy chip and bus):

  1. A time write is confirmed by acknowledgements and a clear BSF, not by reading it back. A password-locked chip acknowledges the writes but ignores them, so with BSF clear the write reads as confirmed without taking effect, and with BSF set it never confirms.
  2. A power loss cutting a write mid-byte leaves that byte undefined (§5.10, p. 95). In the final one-byte year write that byte can read as a valid year; in the burst it can make the next boot rule the chip out. The year-last order covers a cut between bytes. A cut plain write on dev has the same exposure.
  3. A tick at Dec 31 23:59:59 that lands after the burst but before the final year write, which needs the work queue to stall for over a second, would store the old year.
  4. The EEbusy and Refresh waits are in kernel ticks. A build with a coarse tick (not the nRF52840's 32768 Hz) stretches them about tenfold.

Fork issues: ptr727#4, ptr727#33. Iteration history and review rounds: ptr727#35.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The retry path still rejects an all-0xFF RV3028, preventing initial synchronization and configuration.

Review effort: Lite
Findings: None

What changed in this PR

Adds RAK12002 RV3028 RTC discovery and persistent backup-switchover configuration for RAK4631.

Changes:

  • Declares and configures the RV3028 in the board overlay.
  • Adds EEPROM configuration, validation, retries, and fallback handling.
  • Updates RTC bindings, documentation, and Kconfig help.
File Summary
zephcore/​Kconfig Documents RTC configuration behavior.
zephcore/​dts/​bindings/​rtc/​zephcore,rtc-i2c.yaml Defines RV3028 EEPROM configuration.
zephcore/​boards/​nrf52840/​rak4631/​board.overlay Declares and configures the fitted RV3028.
zephcore/​adapters/​clock/​ZephyrRTCDiscover.h Documents discovery and persistence behavior.
zephcore/​adapters/​clock/​ZephyrRTCDiscover.c Implements RV3028 probing, EEPROM handling, fallback, and retries.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ptr727

ptr727 commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Answering Copilot's review overview on 3d399d5 (round), which reported "Findings: None" with this headline:

The retry path still rejects an all-0xFF RV3028, preventing initial synchronization and configuration.

Disproven: a real RV3028 never presents an all-0xFF time block, so the all-0xFF skip cannot reject one.

  • Reset state: the RV-3028-C7 Application Manual (Rev. 1.4, November 2021) says "After reset, all registers are set according to Table in section REGISTER RESET VALUES SUMMARY" (p. 12). That table (§3.18, p. 41) gives the time block 00h-06h as Seconds 00, Minutes 00, Hours 00, Weekday 00, Date 01, Month 01, Year 00. So a chip fresh from power-on reads 2000-01-01 00:00:00, which is valid BCD with no always-zero bit set (§3.2, p. 12). It is adopted on its first read, logged as "time not yet set", and configured. The first sync then writes the time.
  • Operation: once running, every register holds counter values in range, never seven 0xFF bytes.
  • What the skip does catch: an all-0xFF block is an erased EEPROM answering at 0x52, which is the case the skip exists for. Even so, the first save probes once more before giving up.
  • On hardware: on a RAK4631 with a RAK12002, the store configured a chip from its delivery EEPROM and restored the time across reboots and USB-unplug power cuts; see the Testing section and the raw logs. No read on that chip was ever all 0xFF.

@liquidraver

Copy link
Copy Markdown
Owner

Thanks for this. #99 is merged, so this now conflicts in ZephyrRTCDiscover.c, ZephyrRTCDiscover.h and the binding YAML. Please rebase onto dev and drop what #99 already provides (the all-0xFF skip and re-probe, the hardcoded RV3028 always-zero check, the "time unreadable" path); the RAK4631 node should carry zero-mask instead.

A few things I'd like addressed in the rebase:

  1. Run the EEPROM store only on a clean identification. With rtc: identify RTCs by documented always-zero bits; stop skipping real ones #99's rules a device whose first read rules it out and whose second read fails is still adopted, and here adoption means immediate register writes to it.
  2. Known limitation 1 (a year read too high after a switchover during the boot read): please include the BSF clear-before / check-after fix you describe. A forward clock jump is expensive on the mesh, since peers keep the timestamp and only GPS can step us back.
  3. Gate the RV3028 code so boards without rv3028-eeprom-config don't carry it (e.g. DT_ANY_COMPAT_HAS_PROP_STATUS_OKAY). On thinknode_m1 it cost about 1.1 KB before the rebase.
  4. Small: the new RV3028_* defines split the BCD2BIN / BIN2BCD pair.

I'm fine with the persistent EEPROM config for the RAK12002's supercap. Please mention in the overlay comment that any other RV3028 at 0x52 gets the same trickle-charge setting.

ptr727 added a commit to ptr727/liquidraver-ZephCore that referenced this pull request Oct 5, 2026
…idraver#98's rebase

The owner's review of liquidraver#98 asked for a rebase onto dev
dropping what liquidraver#99 provides, plus four changes. This merge carries all of
them:

- liquidraver#99's identify rules replace this branch's all-0xFF skip and re-probe,
  its hardcoded RV3028 always-zero check, and its "time unreadable" path.
  The RAK4631 node now carries zero-mask, as rtc-i2c.dtsi does.
- The EEPROM store runs only on a clean identification (RTC_FOUND). A
  device adopted on a failed second read (RTC_FOUND_GARBLED) is the time
  write-back target but is never configured.
- At an rv3028-eeprom-config descriptor the time is read once more with
  BSF cleared before and checked after (manual 4.2, p. 45: a switchover
  disables the I2C interface, so a read's tail comes back 0xFF, and a tail
  inside the year byte can still be valid BCD). Known limitation 1.
- All RV3028 code is gated on DT_ANY_COMPAT_HAS_PROP_STATUS_OKAY, so a
  board without rv3028-eeprom-config carries none of it.
- The RV3028 defines no longer split the BCD2BIN/BIN2BCD pair.
- The overlay comment says any other RV3028 at 0x52 gets the same
  settings, trickle charging included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/liquidraver-ZephCore that referenced this pull request Oct 5, 2026
Local review of the liquidraver#98 rework:

- A BSF set during the boot time read now also skips the EEPROM store
  for that boot: the EEPROM needs VDD (manual 4.6.8, p. 57), and a
  switchover has just shown it is not steady.
- zephcore_rtc_save() at an rv3028-eeprom-config descriptor writes the
  time block with the same BSF bracket. A switchover mid-write keeps the
  bytes before the cut, a mixed time that reads as valid with PORF clear.
  The write is retried once, and if both fail the year register is
  zeroed, so the next boot reads "time not yet set" instead of
  restoring it.
- The Kconfig help, header and binding now state the clean-identification
  gate, and that the RAM fallback lasts until the next refresh from
  EEPROM, which is daily only while EERD is clear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…abled

The RAK4631 overlay declared no zephcore,rtc-i2c candidate, so a fitted
RAK12002 (an RV3028 at 0x52) was never probed or written, and every
power cut started the node at 1970. Declaring it is not enough: the
RV3028 ships with backup switchover disabled (EEPROM 37h BSM = 00), so a
power cut still loses the time.

The overlay now declares the RV3028 with the zero-mask rtc-i2c.dtsi uses
for it, and a new optional rv3028-eeprom-config descriptor property
holds (register, mask, value) triplets for its EEPROM configuration
registers 35h-37h. The RAK4631 sets direct switchover with the trickle
charger on (TCE, BSM = 01, TCR 3 kOhm), as the RV-3028-C7 Application
Manual (Rev. 1.4) 7.3 prescribes for a capacitor backup, FEDE set as
3.15.6 says it always should be, and CLKOUT off. The configuration has
to live in EEPROM: the chip reloads its configuration RAM from EEPROM at
power-on and every midnight (4.6.2).

The store runs only when a read identified the chip (RTC_FOUND, not an
adoption on a failed second read) and a BSF-checked time read just
before it showed no switchover. It follows 3.15.6 (BSM 00 or 10 for any
EEPROM access): EERD held, switchover off in RAM, each configured byte
compared against the EEPROM with a single-byte read and written only if
it differs, and a closing Refresh that restores the stored switchover
mode, read back on every boot. 36h, the factory trim, is never written.
Switchover is turned back on only once EEbusy reads 0; otherwise it
stays off until a retry or the daily refresh. A failed store sets the
config in RAM (also only once EEbusy reads 0). A failed or skipped store
is retried every 10 minutes, at most 3 times per boot, each retry
identifying the chip again first.

A switchover disables the chip's I2C mid-transfer (4.2), so at a node
with rv3028-eeprom-config the time is read at boot and written on each
sync with BSF cleared before and checked after. A read that saw a
switchover gives no time. A write sets year 00h first and the real year
last, so a power loss cutting it between its first and last byte leaves
year 2000 and the next boot takes no time from it, and a write not
confirmed is repeated every 5 s, up to 12 times, with the time run on.

All of the RV3028 code is compiled only when some node carries
rv3028-eeprom-config, so other boards, thinknode_m1 among them, build
the same RTC object as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727
ptr727 force-pushed the rak4631-rak12002-rtc branch from 3d399d5 to ad2df09 Compare October 5, 2026 15:22
@ptr727

ptr727 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks. Rebased onto dev at 52644a8 as ad2df09. Everything #99 provides is dropped from this PR: the all-0xFF skip and re-probe, the hardcoded RV3028 always-zero check, and the "time unreadable" path. The RAK4631 node now carries zero-mask = [80 80 c0 f8 c0 e0 00], the same as rtc-i2c.dtsi.

  1. EEPROM store only on a clean identification. rv3028_configure() runs only when rtc_identify() returns RTC_FOUND. A device adopted on a failed second read (RTC_FOUND_GARBLED) is the write-back target, as rtc: identify RTCs by documented always-zero bits; stop skipping real ones #99 has it, but nothing is written to its EEPROM. The store is also skipped when a switchover shows during the boot time read (2 below), since the EEPROM needs VDD (§4.6.8, p. 57). A skipped store is retried from the existing 10-minute retry, at most 3 per boot. Each retry runs rtc_identify() again before any write, and stops if two reads rule the chip out.
  2. BSF clear-before / check-after (known limitation 1). At a node with rv3028-eeprom-config, once identified, the time is read again with BSF cleared before (§3.7, p. 22: written as 0, in VDD state) and checked after. If BSF is set or any transfer failed, no time is taken ("time unreadable"). A switchover disables the I2C interface mid-transfer (§4.2, p. 45), so this covers the tail that can start inside the year byte. The same bracket now covers the save:
    • The write order is year 00h, then 00h-05h, then the real year. A power loss cutting the write between its first and last byte leaves year 2000, so the next boot takes no time from it. A cut inside one byte leaves that byte undefined (§5.10); that window is listed under Known limitations in the description.
    • A write that isn't confirmed is repeated every 5 s, up to 12 times, with the time run on.
  3. Gated. All RV3028 code is under #if DT_ANY_COMPAT_HAS_PROP_STATUS_OKAY(zephcore_rtc_i2c, rv3028_eeprom_config). On thinknode_m1 companion, ZephyrRTCDiscover.c.obj is 1628 B of text, the same as dev, with no RV3028 symbols. On the RAK4631 repeater the feature costs about 3 KB of text against dev.
  4. BCD2BIN / BIN2BCD are together again. The RV3028 defines sit inside the gated block.
  5. Overlay comment now ends: "Any other RV3028 module at 0x52 gets the same settings, trickle charging included, kept in its EEPROM."

Two more changes from the rebase's own review:

  • A pre-existing bug, fixed. The RAM fallback after a failed store ran inside LOG_WRN's arguments, so release builds (CONFIG_LOG=n) never ran it. It now runs before the log line.
  • EEbusy. After a Refresh that didn't complete, and for that RAM fallback, switchover is turned back on only once EEbusy reads 0. Otherwise it stays off until a retry or the daily refresh (§3.15.6), rather than coming on during an EEPROM operation.

Testing, on a RAK4631 with a RAK12002, debug repeater builds:

  • The boot restore through the bracketed read and the bracketed save were run at each revision of the rebase.
  • A 66 s USB power cut kept the time (restored exactly the time set before the cut).
  • A warm reboot after the year-last write restored the written time.
  • Release and debug builds of rak4631 (repeater and companion) and thinknode_m1 compile with no warnings.

Keeping ZephCore, MeshCore and Zephyr in step. The same RV3028 rules are in flight in the other two projects:

ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 5, 2026
The impostor check passed a device whose reads failed, so a device at 0x52 that one read had
ruled out could still get EEPROM writes if the confirming read failed. The config store now
runs only when a read of the time registers shows no bit an RV3028 always reads as 0, with a
second read taken when the first rules the device out. This matches the rule adopted for the
ZephCore counterpart (liquidraver/ZephCore#98 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 5, 2026
A switchover to VBACKUP means VDD is unstable, and the EEPROM needs VDD (manual 4.6.8). Once
the chip reads like an RV3028, BSF is cleared, the time registers are read again, and BSF must
still read 0; otherwise the store waits for the retry. BSF is cleared first because a power cut
before this boot leaves it set. This matches the ZephCore counterpart (liquidraver/ZephCore#98).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 5, 2026
Manual 3.15.6 requires BSM = 00 or 10 for any EEPROM read or write, and an operation is still
running while EEbusy reads 1. Neither the restore after a failed Refresh nor the RAM-only
fallback now writes the switchover back unless EEbusy reads 0 within the bound; otherwise it
stays off until the next refresh or retry restores it from the EEPROM. EERD is still cleared.
This is the rule agreed for MeshCore, liquidraver/ZephCore#98 and zephyrproject-rtos/zephyr#121252.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 5, 2026
A switchover to VBACKUP disables and resets the RV3028's I2C interface (manual 4.2, 5.10), so
the rest of a read comes back as 1s, and a byte can still decode to a valid but larger value.
The burst read is now used only if BSF reads 0 after it. A set BSF is cleared and the read
retried once, which also clears a flag left by an earlier power cut. A rejected read is never
used: once a burst read has succeeded, the time runs on from the last good one.

This matches the switchover guard adopted for the ZephCore counterpart
(liquidraver/ZephCore#98 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 5, 2026
Review fixes for the BSF read guard:
- A read rejected for BSF before any good read fell through to the field-by-field getters,
  which have no guard; it now uses the fallback clock.
- The BSF clear's result was ignored; a BSF that cannot be cleared is now reported as such.
- The run-on from the last good time wrapped with millis() after 49.7 days; the reference now
  moves forward on each call.

The time write is bracketed the same way (mirroring liquidraver/ZephCore#98): BSF is cleared,
the clock registers are burst-written, and BSF must still read 0, with one retry. A switchover
mid-write leaves a mixed time that reads back as valid, so after a second failure the year is
zeroed for the next boot to read 2000, and this boot runs on from the time being set until a
write succeeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 5, 2026
Review round 3: a power cut during a time write leaves a mixed time that the next boot accepts
as valid, and no retry survives the brown-out. The year is now zeroed first, the other clock
registers written from Seconds (which resets the prescaler, manual 3.3), and the year written
last, so a cut write reads back as year 2000, the chip's reset state (3.18), rather than a
mixed time. The order is agreed with liquidraver/ZephCore#98, which uses the same marker.

Also name the unconfirmed-write case in the rejected-read comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 5, 2026
Year 00 is the chip's reset state (manual 3.18) and what a cut time write now leaves, so a read
showing it is rejected like any other: the time runs on from the last good or set one, or comes
from the fallback clock, as with no RTC. This matches zephyrproject-rtos/zephyr's RV3028 driver
(-ENODATA for year 00) and liquidraver/ZephCore#98, and removes the step back from the fallback
seed to 2000 on the first read of an unset chip.

Also make the write-order comment exact: it departs from the one-access rule (4.5) on purpose,
and only a tick before the burst can carry into the year.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 5, 2026
Agreed with liquidraver/ZephCore#98 and zephyrproject-rtos/zephyr#121252: the chip state is
shared (the year is zeroed first and written last, so year 00 means never set or a cut write),
and each platform reports it its own way. MeshCore's clock has no 'not set' value, and the
fallback clock restarts every boot, which made a never-set node's adverts step back and be
dropped as replays after each reboot. Counting on from the chip's 2000-based time, as dev does,
keeps them monotonic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 5, 2026
Reverts 8c1e7c8. Its reason, a never-set repeater's adverts stepping back after a reboot,
is cosmetic: a repeater forwards every packet the same with a wrong clock, and its time only
stamps its own adverts (https://swaits.com/does-repeater-time-matter-in-meshcore/), while the
default repeater setup sets the time after configuration. With no platform reason left to
differ, MeshCore keeps the rule shared with liquidraver/ZephCore#98 and
zephyrproject-rtos/zephyr#121252: a never-set node behaves like a node without an RTC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@liquidraver
liquidraver merged commit cdd6cc6 into liquidraver:dev Oct 5, 2026
1 check passed
ptr727 added a commit to ptr727/liquidraver-ZephCore that referenced this pull request Oct 5, 2026
liquidraver#98 moved the time block into rtc_time_block(), shared by the save and
the RV3028 write retry, so the one-hot weekday ternary moves there. Both
descriptor fields and binding properties are kept, and the block's
comment no longer calls the weekday byte BCD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727
ptr727 deleted the rak4631-rak12002-rtc branch October 5, 2026 21:59
ptr727 added a commit to ptr727/liquidraver-ZephCore that referenced this pull request Oct 6, 2026
dev brought liquidraver#99's RTC identification (rtc_identify() and its verdicts),
liquidraver#98's RAK12002 and RV3028 EEPROM configuration, liquidraver#100, and the rename of
docs/Repeater_CLI_commands.md to docs/CLI_commands.md. The hw docs moved
with the rename without conflict.

ZephyrRTCDiscover.c conflicted where hw recorded a report state at the
inline reads liquidraver#99 replaced. dev's logic is taken whole, and each verdict
now maps to a report state: an address NACK or RTC_NOT_THIS is absent,
RTC_FOUND and RTC_FOUND_GARBLED are present. rtc_identify() gains
RTC_UNREAD for a bus that is not ready or a read failing other than with
-EIO, which keeps hw's "unprobed" for those; discovery skips it exactly
as it skips RTC_ABSENT.

liquidraver#99's all-0xFF skip is neither absent nor present, so it gets its own
state, rendered "all 0xff". It counts with unprobed toward the summary,
now worded "unsettled", since neither settles whether an RTC is fitted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/zephyrproject-rtos-zephyr that referenced this pull request Oct 6, 2026
On a switchover from VDD to VBACKUP the RV-3028-C7 disables and resets
its I2C interface, so an access in progress is cut short (Application
Manual Rev. 1.4, 4.2 and 5.10). The rest of a read then returns 1s, or a
byte that changed meanwhile, and rv3028_get_time() returned whatever it
decoded. A write cut short leaves a mix of the old and new time, which
the next read returned as valid. This can happen whenever the switchover
is enabled and VDD fails during an access.

The switchover sets BSF in the Status register, which can be cleared
only on VDD (3.7). Bracket the time register accesses with it: clear a
BSF left by an earlier switchover, read or write the time registers,
then read Status again and return -EIO if BSF is set. set_time() then
also leaves PORF set. get_time() additionally returns -EIO for a BCD
nibble above 9 or a time that rtc_utils_validate_rtc_time() rejects.
BSF is written only when it is already set, and the bracket runs under
the MFD lock. Retrying is left to the caller.

A BSF found set by get_time() can also come from a power loss during a
set_time() that never returned. Each byte of a write is stored as it is
acknowledged, so such a write leaves a mix of the old and new time.
set_time() therefore first writes A0h, a value that is not BCD, to the
year register, then 00h-06h in one access as before (4.5). The year is
the last byte, so a write cut short leaves A0h. The year counter
increments a left mark like a BCD value, so get_time() reports any year
from A0h as not set (-ENODATA), except FFh, which is what a read cut
short returns.

The same write order and year handling are used by MeshCore and ZephCore
for RV3028; the three are kept consistent in behaviour.

Tested on a RAK4631 with a RAK12002 (RV-3028-C7). A write to 00h cut
short by the I2C bus timeout (4.5.1), the interface reset that software
can trigger, kept the bytes acknowledged before the timeout, and the
year register held A0h. With this change, year 2000 was set and read
back, A0h followed by a write cut short by the timeout read as -ENODATA,
and a normal set and read succeeded. Across a 31 December rollover the
year register went from A0h to A1h, A9h to B0h and FFh to F0h, and each
read as -ENODATA. A switchover during an access was not reproduced.
Built for native_sim/native and native_sim/native/64 with the RTC
build_all I2C test in the ci-base v0.29.4 image.

Link: https://www.microcrystal.com/fileadmin/Media/Products/RTC/App.Manual/RV-3028-C7_App-Manual.pdf
Link: meshcore-dev/MeshCore#3421
Link: liquidraver/ZephCore#98
Assisted-by: Claude:claude-opus-5.5
Signed-off-by: Pieter Viljoen <pieter@viljoen.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 6, 2026
…bled transfers

getCurrentTime() built its DateTime from six Melopero getters, seven I2C
transactions read most-significant-first while the counters run, so an
hour boundary crossed between the hour and minute reads returned a time
an hour in the past. Read the seven clock registers in one burst, which
the chip holds coherent (RV-3028-C7 Application Manual Rev 1.4, 4.5),
and validate the BCD nibbles, the month and DateTime::isValid().

A switchover to VBACKUP disables and resets the chip's I2C interface
(4.2, 5.10), so the rest of a transfer reads as 1s and a byte can still
decode to a valid but larger value. A read is used only if BSF reads 0
after it; a set BSF is cleared (3.7) and the read retried once, which
also clears a flag left by an earlier power cut. A rejected read is
never used: the time runs on from the last accepted or set time, or
before there is one comes from the fallback clock, as with no RTC. The
field-by-field getters are no longer used, since a failed read decodes
as year 2165, past RTClib's month table.

setCurrentTime() writes inside the same BSF bracket. It marks the year
A0h, which is not BCD, then writes Seconds through Year in one access
(4.5). Bytes acked before an interface reset are kept, so a write cut
part way leaves the A0h mark rather than a mixed time that passes for
the real one. The mark, or the mark counted on by a new year (A0h-FEh),
reads as a clock not yet set; FFh stays a failed read, as a read cut by
an interface reset returns it. Year 00 reads as 2000. An unconfirmed
write is repeated from getCurrentTime() until one is confirmed.

This is part of a coordinated RV3028 update, kept consistent in
behaviour across Zephyr (zephyrproject-rtos/zephyr#121389), MeshCore
and ZephCore (follow-up to liquidraver/ZephCore#98). It replaces the
year-00 split write that all three adopted on 2026-10-05.

Also log RTC detection for DS3231 and RV3028, which had none, plus a
line naming the driver that was bound.

Tested on a RAK4631 with a RAK12002: time set and read back, 2000-01-01
set and read back across a reboot, a staged cut write (A0h plus a
partial burst) and years A1h and B0h use the fallback clock, FFh is
logged as a failed read, and a ~60 s power cut keeps the time. Built
for ESP32-S3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 6, 2026
…bled transfers

getCurrentTime() built its DateTime from six Melopero getters, seven I2C
transactions read most-significant-first while the counters run, so an
hour boundary crossed between the hour and minute reads returned a time
an hour in the past. Read the seven clock registers in one burst, which
the chip holds coherent (RV-3028-C7 Application Manual Rev 1.4, 4.5),
and validate the BCD nibbles, the month and DateTime::isValid().

A switchover to VBACKUP disables and resets the chip's I2C interface
(4.2, 5.10), so the rest of a transfer reads as 1s and a byte can still
decode to a valid but larger value. A read is used only if BSF reads 0
after it; a set BSF is cleared (3.7) and the read retried once, which
also clears a flag left by an earlier power cut. A rejected read is
never used: the time runs on from the last accepted or set time, or
before there is one comes from the fallback clock, as with no RTC. The
field-by-field getters are no longer used, since a failed read decodes
as year 2165, past RTClib's month table.

setCurrentTime() writes inside the same BSF bracket. It marks the year
A0h, which is not BCD, then writes Seconds through Year in one access
(4.5). Bytes acked before an interface reset are kept, so a write cut
part way leaves the A0h mark rather than a mixed time that passes for
the real one. The mark, or the mark counted on by a new year (A0h-FEh),
reads as a clock not yet set; FFh stays a failed read, as a read cut by
an interface reset returns it. Year 00 reads as 2000. An unconfirmed
write is repeated from getCurrentTime() until one is confirmed. A time
outside 2000-2099 is never written, since the chip counts years 00-99
and 2100 would encode as the A0h mark; the clock runs on from it
without the RTC.

This is part of a coordinated RV3028 update, kept consistent in
behaviour across Zephyr (zephyrproject-rtos/zephyr#121389), MeshCore
and ZephCore (follow-up to liquidraver/ZephCore#98). It replaces the
year-00 split write that all three adopted on 2026-10-05.

Also log RTC detection for DS3231 and RV3028, which had none, plus a
line naming the driver that was bound.

Tested on a RAK4631 with a RAK12002: time set and read back, 2000-01-01
set and read back across a reboot, a staged cut write (A0h plus a
partial burst) and years A1h and B0h use the fallback clock, FFh is
logged as a failed read, a 2100 set leaves the RTC untouched, and a ~60 s power cut keeps the time. Built
for ESP32-S3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 6, 2026
…bled transfers

getCurrentTime() built its DateTime from six Melopero getters, seven I2C
transactions read most-significant-first while the counters run, so an
hour boundary crossed between the hour and minute reads returned a time
an hour in the past. Read the seven clock registers in one burst, which
the chip holds coherent (RV-3028-C7 Application Manual Rev 1.4, 4.5),
and validate the BCD nibbles, the month and DateTime::isValid().

A switchover to VBACKUP disables and resets the chip's I2C interface
(4.2, 5.10), so the rest of a transfer reads as 1s and a byte can still
decode to a valid but larger value. A read is used only if BSF reads 0
after it; a set BSF is cleared (3.7) and the read retried once, which
also clears a flag left by an earlier power cut. A rejected read is
never used: the time runs on from the last accepted or set time, or
before there is one comes from the fallback clock, as with no RTC. The
field-by-field getters are no longer used, since a failed read decodes
as year 2165, past RTClib's month table.

setCurrentTime() writes inside the same BSF bracket. It marks the year
A0h, which is not BCD, then writes Seconds through Year in one access
(4.5). Bytes acked before an interface reset are kept, so a write cut
part way leaves the A0h mark rather than a mixed time that passes for
the real one. The mark, or the mark counted on by a new year (A0h-FEh),
reads as a clock not yet set; FFh stays a failed read, as a read cut by
an interface reset returns it. Year 00 reads as 2000. An unconfirmed
write is repeated from getCurrentTime() until one is confirmed. A time
outside 2000-2099 is never written, since the chip counts years 00-99
and 2100 would encode as the A0h mark; the clock runs on from it
without the RTC. The weekday is now DateTime::dayOfTheWeek(); the
formula carried over from dev stored the wrong weekday on most days.

This is part of a coordinated RV3028 update, kept consistent in
behaviour across Zephyr (zephyrproject-rtos/zephyr#121389), MeshCore
and ZephCore (follow-up to liquidraver/ZephCore#98). It replaces the
year-00 split write that all three adopted on 2026-10-05.

Also log RTC detection for DS3231 and RV3028, which had none, plus a
line naming the driver that was bound.

Tested on a RAK4631 with a RAK12002: time set and read back, 2000-01-01
set and read back across a reboot, a staged cut write (A0h plus a
partial burst) and years A1h and B0h use the fallback clock, FFh is
logged as a failed read, a 2100 set leaves the RTC untouched, the stored weekday is right, and a ~60 s power cut keeps the time. Built
for ESP32-S3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 6, 2026
…bled transfers

getCurrentTime() built its DateTime from six Melopero getters, seven I2C
transactions read most-significant-first while the counters run, so an
hour boundary crossed between the hour and minute reads returned a time
an hour in the past. Read the seven clock registers in one burst, which
the chip holds coherent (RV-3028-C7 Application Manual Rev 1.4, 4.5),
and validate the BCD nibbles, the month and DateTime::isValid().

A switchover to VBACKUP disables and resets the chip's I2C interface
(4.2, 5.10), so the rest of a transfer reads as 1s and a byte can still
decode to a valid but larger value. A read is used only if BSF reads 0
after it; a set BSF is cleared (3.7) and the read retried once, which
also clears a flag left by an earlier power cut. Control 2 is read in the
same access, and 12 hour mode, which begin() leaves unchecked, is cleared
the same way (the chip converts Hours itself, manual 02h), so a PM hour
cannot decode as a wrong 24 hour one. A rejected read is
never used: the time runs on from the last accepted or set time, or
before there is one comes from the fallback clock, as with no RTC. The
field-by-field getters are no longer used, since a failed read decodes
as year 2165, past RTClib's month table.

setCurrentTime() writes inside the same bracket. It marks the year
A0h, which is not BCD, then writes Seconds through Year in one access
(4.5). Bytes acked before an interface reset are kept, so a write cut
part way leaves the A0h mark rather than a mixed time that passes for
the real one. The mark, or the mark counted on by a new year (A0h-FEh),
reads as a clock not yet set; FFh stays a failed read, as a read cut by
an interface reset returns it. Year 00 reads as 2000. An unconfirmed
write is repeated from getCurrentTime() until one is confirmed. A time
outside 2000-2099 is never written, since the chip counts years 00-99
and 2100 would encode as the A0h mark; the clock runs on from it
without the RTC. The weekday is now DateTime::dayOfTheWeek(); the
formula carried over from dev stored the wrong weekday on most days.

This is part of a coordinated RV3028 update, kept consistent in
behaviour across Zephyr (zephyrproject-rtos/zephyr#121389), MeshCore
and ZephCore (follow-up to liquidraver/ZephCore#98). It replaces the
year-00 split write that all three adopted on 2026-10-05.

Also log RTC detection for DS3231 and RV3028, which had none, plus a
line naming the driver that was bound.

Tested on a RAK4631 with a RAK12002: time set and read back, 2000-01-01
set and read back across a reboot, a staged cut write (A0h plus a
partial burst) and years A1h and B0h use the fallback clock, FFh is
logged as a failed read, a 2100 set leaves the RTC untouched, the stored weekday is right, 12 hour mode is cleared on read and
write, and a ~60 s power cut keeps the time. Built
for ESP32-S3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/liquidraver-ZephCore that referenced this pull request Oct 6, 2026
The RV3028 time write set year 00h, burst-wrote 00h-05h, then wrote the
real year, so a write cut short left year 2000 and the next boot took no
time from it. That split the write into separate accesses against the
manual's one-access rule (Application Manual Rev 1.4, 4.5), relying on
the Seconds write restarting the prescaler, and used a real date as the
"not set" mark.

The write now sets 06h to A0h, which is not BCD, then writes all seven
registers 00h-06h in one burst, year last, inside the existing BSF
bracket. The chip keeps each byte as it is acknowledged, so a write cut
between the mark and the year byte leaves the mark. On read at an
rv3028-eeprom-config descriptor, a year of A0h-FEh gives no time: a New
Year counts a left mark on (A0h to A1h, A9h to B0h). FFh stays a failed
read, since a read cut short returns 1s and the year is the last byte.

The save also refuses a year outside 2000-2099, for every chip, because
ZephCore writes the year as two BCD digits and no century bit (the
DS3231 and PCF8563 century bits are written as 0 and masked off on
read). It encoded y % 100, so 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: `time 900000000` (1998) was restored at the next boot as 2098,
past the 2025 floor. At an rv3028-eeprom-config descriptor a refused
year is marked A0h instead, so the next boot takes no time rather than
the older time the chip still holds; the mark gets the BSF bracket and
the same 5 s repeat as a time write, sharing its 13-attempt cap, and a
repeat whose run-on time leaves 2000-2099 marks instead. Other chips
keep their older time.

The header contract and the binding describe both; the Kconfig help and
ARCHITECTURE.md describe the 2000-2099 range, and ARCHITECTURE.md the
refused-year mark.

The same write order, year handling and 2000-2099 range are used by
Zephyr and MeshCore for the RV3028; the three are kept consistent in
behaviour.

Link: zephyrproject-rtos/zephyr#121389
Link: meshcore-dev/MeshCore#3421
Link: liquidraver#98

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 6, 2026
…bled transfers

getCurrentTime() built its DateTime from six Melopero getters, seven I2C
transactions read most-significant-first while the counters run, so an
hour boundary crossed between the hour and minute reads returned a time
an hour in the past. Read the seven clock registers in one burst, which
the chip holds coherent (RV-3028-C7 Application Manual Rev 1.4, 4.5),
and validate the BCD nibbles, the month and DateTime::isValid().

A switchover to VBACKUP disables and resets the chip's I2C interface
(4.2, 5.10), so the rest of a transfer reads as 1s and a byte can still
decode to a valid but larger value. A read is used only if BSF reads 0
after it; a set BSF is cleared (3.7) and the read retried once, which
also clears a flag left by an earlier power cut. Control 2 is read in the
same access, and 12 hour mode, which begin() leaves unchecked, is cleared
the same way (the chip converts Hours itself, manual 02h), so a PM hour
cannot decode as a wrong 24 hour one. Control 2's RESET bit always
reads 0, so a read showing it set was cut short and is not written back;
Melopero's unchecked set24HourMode() is no longer called from begin(). A rejected read is
never used: the time runs on from the last accepted or set time, or
before there is one comes from the fallback clock, as with no RTC. The
field-by-field getters are no longer used, since a failed read decodes
as year 2165, past RTClib's month table.

setCurrentTime() writes inside the same bracket. It marks the year
A0h, which is not BCD, then writes Seconds through Year in one access
(4.5). Bytes acked before an interface reset are kept, so a write cut
part way leaves the A0h mark rather than a mixed time that passes for
the real one. The mark, or the mark counted on by a new year (A0h-FEh),
reads as a clock not yet set; FFh stays a failed read, as a read cut by
an interface reset returns it. Year 00 reads as 2000. An unconfirmed
write is repeated from getCurrentTime() until one is confirmed. A time
outside 2000-2099 is never written, since the chip counts years 00-99
and 2100 would encode as the A0h mark; the clock runs on from it
without the RTC. The weekday is now DateTime::dayOfTheWeek(); the
formula carried over from dev stored the wrong weekday on most days.

This is part of a coordinated RV3028 update, kept consistent in
behaviour across Zephyr (zephyrproject-rtos/zephyr#121389), MeshCore
and ZephCore (follow-up to liquidraver/ZephCore#98). It replaces the
year-00 split write that all three adopted on 2026-10-05.

Also log RTC detection for DS3231 and RV3028, which had none, plus a
line naming the driver that was bound.

Tested on a RAK4631 with a RAK12002: time set and read back, 2000-01-01
set and read back across a reboot, a staged cut write (A0h plus a
partial burst) and years A1h and B0h use the fallback clock, FFh is
logged as a failed read, a 2100 set leaves the RTC untouched, the stored weekday is right, 12 hour mode is cleared on read and
write with the other Control 2 bits kept, and a ~60 s power cut keeps the time. Built
for ESP32-S3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 added a commit to ptr727/zephyrproject-rtos-zephyr that referenced this pull request Oct 6, 2026
On a switchover from VDD to VBACKUP the RV-3028-C7 disables and resets
its I2C interface, so an access in progress is cut short (Application
Manual Rev. 1.4, 4.2 and 5.10). The rest of a read then returns 1s, or a
byte that changed meanwhile, and rv3028_get_time() returned whatever it
decoded. A write cut short leaves a mix of the old and new time, which
the next read returned as valid. This can happen whenever the switchover
is enabled and VDD fails during an access.

The switchover sets BSF in the Status register, which can be cleared
only on VDD (3.7). Bracket the time register accesses with it: clear a
BSF left by an earlier switchover, read or write the time registers,
then read Status again and return -EIO if BSF is set. set_time() then
also leaves PORF set. get_time() additionally returns -EIO for a BCD
nibble above 9 or a time that rtc_utils_validate_rtc_time() rejects. BSF
is written only when it is already set, and the bracket runs under the
MFD lock. Status flags, BSF and PORF, are cleared by writing 0 to the
flag and 1 to the others, which leaves them unchanged (3.7), so no read
precedes the write: a flag set in between is not lost, and a read cut
short is not written back. Retrying is left to the caller.

A BSF found set by get_time() can also come from a power loss during a
set_time() that never returned. Each byte of a write is stored as it is
acknowledged, so such a write leaves a mix of the old and new time.
set_time() therefore first writes A0h, a value that is not BCD, to the
year register, then 00h-06h in one access as before (4.5). The year is
the last byte, so a write cut short leaves A0h. The year counter
increments a left mark like a BCD value, so get_time() reports any year
from A0h as not set (-ENODATA), except FFh, which is what a read cut
short returns.

The same write order and year handling are used by MeshCore and ZephCore
for RV3028; the three are kept consistent in behaviour.

Tested on a RAK4631 with a RAK12002 (RV-3028-C7). A write to 00h cut
short by the I2C bus timeout (4.5.1), the interface reset that software
can trigger, kept the bytes acknowledged before the timeout, and the
year register held A0h. With this change, year 2000 was set and read
back, A0h followed by a write cut short by the timeout read as -ENODATA,
and a normal set and read succeeded. Across a 31 December rollover the
year register went from A0h to A1h, A9h to B0h and FFh to F0h, and each
read as -ENODATA. Writing 7Eh and then 01h to a clear Status register
left every flag 0, and an update flag set before set_time() was still
set after it. A switchover during an access was not reproduced. Built
for native_sim/native and native_sim/native/64 with the RTC build_all
I2C test in the ci-base v0.29.4 image.

Link: https://www.microcrystal.com/fileadmin/Media/Products/RTC/App.Manual/RV-3028-C7_App-Manual.pdf
Link: meshcore-dev/MeshCore#3421
Link: liquidraver/ZephCore#98
Assisted-by: Claude:claude-opus-5.5
Signed-off-by: Pieter Viljoen <pieter@viljoen.com>
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Oct 7, 2026
…bled transfers

getCurrentTime() built its DateTime from six Melopero getters, seven I2C
transactions read most-significant-first while the counters run, so an
hour boundary crossed between the hour and minute reads returned a time
an hour in the past. Read the seven clock registers in one burst, which
the chip holds coherent (RV-3028-C7 Application Manual Rev 1.4, 4.5),
and validate the BCD nibbles, the month and DateTime::isValid().

A switchover to VBACKUP disables and resets the chip's I2C interface
(4.2, 5.10), so the rest of a transfer reads as 1s and a byte can still
decode to a valid but larger value. A read is used only if BSF reads 0
after it; a set BSF is cleared (3.7) by writing 0 to it alone, since
writing 1 leaves a flag unchanged, and the read retried once, which also
clears a flag left by an earlier power cut. Control 2 is read in the
same access, and 12 hour mode, which begin() leaves unchecked, is
cleared the same way (the chip converts Hours itself, manual 02h), so a
PM hour cannot decode as a wrong 24 hour one. Control 2's RESET bit
always reads 0, so a read showing it set was cut short and is not
written back; Melopero's unchecked set24HourMode() is no longer called
from begin(). A rejected read is never used: the time runs on from the
last accepted or set time, or before there is one comes from the
fallback clock, as with no RTC. The field-by-field getters are no longer
used, since a failed read decodes as year 2165, past RTClib's month
table.

setCurrentTime() writes inside the same bracket. It marks the year A0h,
which is not BCD, then writes Seconds through Year in one access (4.5).
Bytes acked before an interface reset are kept, so a write cut part way
leaves the A0h mark rather than a mixed time that passes for the real
one. The mark, or the mark counted on by a new year (A0h-FEh), reads as
a clock not yet set; FFh stays a failed read, as a read cut by an
interface reset returns it. Year 00 reads as 2000. An unconfirmed write
is retried every 5 s, at most 13 times in all, so a bus that is down
cannot stall each caller of getCurrentTime(). A time outside 2000-2099
is never written, since the chip counts years 00-99 and 2100 would
encode as the A0h mark; the clock runs on from it without the RTC. The
weekday is now DateTime::dayOfTheWeek(); the formula carried over from
dev stored the wrong weekday on most days.

This is part of a coordinated RV3028 update, kept consistent in
behaviour across Zephyr (zephyrproject-rtos/zephyr#121389), MeshCore and
ZephCore (follow-up to liquidraver/ZephCore#98). It replaces the year-00
split write that all three adopted on 2026-10-05.

Also log RTC detection for DS3231 and RV3028, which had none, plus a
line naming the driver that was bound.

Tested on a RAK4631 with a RAK12002: time set and read back, 2000-01-01
set and read back across a reboot, a staged cut write (A0h plus a
partial burst) and years A1h and B0h use the fallback clock, FFh is
logged as a failed read, a 2100 set leaves the RTC untouched, the stored
weekday is right, 12 hour mode is cleared on read and write with the
other Control 2 bits kept, and a ~60 s power cut keeps the time. Built
for ESP32-S3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants