Skip to content

Store RV3028 backup switchover and trickle charger config in EEPROM 🤖🤖 - #3545

Open
ptr727 wants to merge 1 commit into
meshcore-dev:devfrom
ptr727:fix/rv3028-eeprom-config
Open

ptr727 wants to merge 1 commit into
meshcore-dev:devfrom
ptr727:fix/rv3028-eeprom-config

Conversation

@ptr727

@ptr727 ptr727 commented Oct 4, 2026 •

Copy link
Copy Markdown

References used throughout:

  • The manual: RV-3028-C7 Application Manual, Micro Crystal, Rev. 1.4, November 2021, from the Documents section of the product page: https://www.microcrystal.com/en/products/real-time-clock-rtc-modules/rv-3028-c7. Every "§" section number and "p." page number below is that document's.
  • The code: links point at fixed commits:
    • before: upstream dev 3e3150c8, the base. AutoDiscoverRTCClock is unchanged since fad0ffb7, where the dev hardware rows below ran
    • after: this PR's commit 23b23b24
    • the RTC library MeshCore builds against: Melopero RV3028 Arduino library, tag 1.2.0

The defect

AutoDiscoverRTCClock::begin() configured the RV3028 with two plain register writes (L38-L39):

rtc_rv3028.writeToRegister(0x35, 0x00);
rtc_rv3028.writeToRegister(0x37, 0xB4);

Melopero's writeToRegister() is a plain I2C write, so these set only the RAM mirror of EEPROM-backed configuration registers:

  • The setting doesn't last. With EERD = 0, which is the default (Control 1, §3.7, p. 23), the chip reloads that mirror 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 factory EEPROM undoes it. It holds BSM = 00 and TCE = 0 (§3.15.6, p. 39), so the backup switchover and the trickle charger switch off at the first midnight after boot. After that a power cut loses the time, and the supercap is no longer charged.
  • The calibration bit gets overwritten. 0xB4 also forces 37h bit 7, which is EEOffset[0], the LSB of the factory frequency calibration (§3.15.6, p. 39).

The mode itself is right. §7.3 (p. 105) specifies DSM with the trickle charger for a capacitor backup, and adds: "Power Management settings have to be stored in EEPROM for permanent configuration".

The fix

The config is stored in EEPROM following the manual, including §3.15.6 (p. 39): BSM must be 00 or 10 for any EEPROM read or write. Links are to 23b23b24.

  1. Identify before writing. rv3028Configure() writes nothing unless rv3028Identify() gets a read of the time registers 00h-06h with no bit an RV3028 always reads as 0 (§3.2, p. 12). A device at 0x52 that is not an RV3028, such as a 24-series EEPROM, gets none of the config writes.
    • A second read is taken only when the first rules the device out, so one corrupted read cannot hide a real RV3028.
    • Two reads that rule it out end the config only if BSF reads 0. A switchover releases the bus, which reads as 1s (§4.2, p. 45).
    • A failed read writes nothing and is retried like a failed store.
    • These masks check only those bits, so a foreign device that reads them as 0 still passes. §3.14 (p. 35) gives no fixed ID value to check instead.
  2. Boot read on VDD. rv3028OnVdd() clears BSF (§3.7, p. 22), reads 00h-06h again, and requires BSF still 0. A switchover in between means VDD is unstable, and an EEPROM write needs VDD (§4.6.8, p. 57), so the store waits for a retry. BSF is cleared first because a power cut before this boot leaves it set.
  3. Hold off refresh. rv3028StoreConfig() sets EERD = 1, then waits for EEbusy = 0 (§4.6.7, p. 56). This also covers the ~66 ms POR refresh (§4.6.1, p. 54).
  4. Switchover off. It saves RAM 37h, then writes it back with BSM = 00, so the switchover is off while the EEPROM is accessed (§3.15.6, p. 39).
  5. Compare and write per byte. For each byte in rv3028_config, it reads the EEPROM copy with a single-byte read (EECMD 22h, §4.6.6, p. 55). It writes with a single-byte write (21h, §4.6.5, p. 55) only if the byte differs (rv3028EepromRead/Write()). The bits written:
    • 35h: CLKOE = 0 (§3.15.4, p. 37).
    • 37h: TCE = 1, FEDE = 1, BSM = 01, TCR = 00 (§3.15.6, p. 39). The manual says FEDE "should always be set to 1", and the old 0xB4 set it too.
    • EEOffset[0] and BSIE keep the chip's own values. 36h, the rest of the factory trim, is never written.
  6. Refresh and verify. It sends a Refresh (12h, §4.6.4, p. 54), which reloads RAM from EEPROM and brings back the stored switchover mode. Then it reads the config back, even when nothing was written, so every boot confirms the switchover came back.
  7. If the Refresh failed, restore only once EEbusy is clear. It waits the §4.6.7 10 ms (a command whose write reported failure may still have been latched), then writes the saved 37h back only if EEbusy reads 0 (L139-L142). While an EEPROM operation may still be running the switchover stays off, as §3.15.6 requires, until a later attempt succeeds.
  8. Release refresh. It clears EERD. Control1 is read again first, because the chip clears TE itself when a single-shot countdown ends. If that re-read fails, Control1 is left alone and the store reports failure. Clearing EERD while EEbusy may still be set is a judgement, not documented behaviour (the §4.6.7 flowchart re-enables refresh after EEbusy = 0): it starts no EEPROM operation, and the next automatic refresh is at midnight.

Waits and transfers:

  • rv3028EepromCommand() waits per §4.6.7 (p. 56): 1 ms after a read or Refresh, 10 ms after a write. Each gets an extra 1 ms, because Arduino delay() can return early on some cores.
  • rv3028EepromIdle() polls for about 100 ms, and ends the wait on the first failed status read, so a dead bus is not polled through each transfer's timeout.
  • Every transfer is checked through TwoWire directly (rv3028Read/Write()). Melopero's readFromRegister() ignores the I2C results and returns 0xFF on a failed transfer, and a failed read must never be written back.

Failure handling: if the store fails, rv3028Configure() sets the RAM mirror as before with a checked read-modify-write (rv3028SetRam()), but only once EEbusy reads 0 (L244). A RAM-only config lasts only until the next refresh, so getCurrentTime() re-runs the whole attempt, identification included, every 10 minutes, at most 3 times per boot (L207-L214). The cap is there because a password-locked chip never succeeds, and each attempt writes to it again. Failures are logged with MESH_DEBUG_PRINTLN, whose arguments carry no side effects, so release builds run the same paths.

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

Notes for review

  • Shared with ZephCore and Zephyr. The same sequence, identification gate, BSF-bracketed boot read and EEbusy rule are in liquidraver/ZephCore#98, and the §3.15.6 EEPROM access and EEbusy rule in zephyrproject-rtos/zephyr#121252. The three were aligned deliberately. They differ only where the platform does: MeshCore retries from getCurrentTime(), ZephCore from a work-queue timer, and Zephyr leaves retry to the caller.
  • Why §3.15.6 instead of the vendor driver's Update. Micro Crystal's Linux driver and Zephyr's drivers/mfd/mfd_rv3028.c on main run a Refresh and a whole-block Update (§4.6.3, p. 54) with BSM live. That was this branch's first version too, f0af008a. Testing on two boards found no spurious switchovers either way (see Testing), so §3.15.6 was chosen on the manual:
    • It is the only sequence that stays valid when the backup sits above VDD. In DSM (§4.2.2, p. 46) the chip then runs from backup permanently, and an EEPROM write needs VDD (§4.6.8, p. 57).
    • It never rewrites the 36h trim.
    • Its cost: the switchover is off for the duration of the EEPROM access on each boot, plus up to 2 ms for DSM to react once re-enabled (§4.2.2, p. 46). That was not measured in MeshCore. The ZephCore implementation of the same sequence measured 14.3 ms on a boot that wrote nothing, and 52.2 ms on the one store that wrote both bytes. A full power cut inside that window loses the time.
  • Behaviour change: the settings now outlive MeshCore. A module flashed once keeps DSM and trickle charging after a reflash to other firmware, or after a move to another carrier. That is the point of the fix. It matters only for a carrier whose backup element is not rechargeable.
  • Retry limits. After the 4th failed attempt nothing retries until the next boot. If that last attempt left the switchover off (EEbusy stuck, or the RAM fallback lost to a bus fault) on a part whose EEPROM still holds the factory BSM = 00, it stays off until the next boot. Reaching that state takes 4 failures over about 30 minutes.
  • 35h: the old 0x00 also cleared CLKSY, PORIE and FD in RAM. Those now keep the chip's values (§3.15.4, p. 37). With CLKOE = 0 the pin is held low, so CLKSY and FD have no effect. PORIE stays at its factory 0. Zephyr stores FD = 111 as well, which MeshCore accepts as found: §7.3 note 5 (p. 105) allows either way of turning CLKOUT off.
  • Out of scope, pre-existing:
    • begin() still adopts anything that ACKs at 0x52 as the clock, reading it as the time and writing it on every sync, and does the same for the other three RTC addresses. That is fixed separately in Adopt an RTC only if its time registers read like that chip 🤖🤖 #3544. Here only the new config writes are gated by identification.
    • On every board it forces DSM and trickle charging, which would charge a primary cell on a carrier that has one.
    • set24HourMode() still uses Melopero's unchecked read-modify-write on Control2, on any device at 0x52.

Testing

Builds: RAK_4631_repeater (nRF52840) and heltec_v4_repeater (ESP32-S3) at the final iteration head; RAK_11310_repeater (RP2040) at aaf190db. Release and MESH_DEBUG=1, with no new warnings.

Hardware setup:

  • Board and RTC: a RAK4631 with a RAK12002 (RV3028).
  • No GPS time sync: the board also carries a RAK12501 (Quectel L76K, on the UART). It was indoors on the bench with no fix, so GPS never set the clock during these tests.
  • Power: USB only, no battery.
  • Diagnostic CLI: each build had a throwaway serial CLI added, published as test/rv3028-diag (290cfc5b, never merged). Its rv dump prints RAM and EEPROM 35h/37h, Control 1 and status 0Eh. Its rv set writes the time registers, so the RTC could be stepped to 23:59:50 to pass midnight in seconds. Its rv factory restores EEPROM 35h/37h to their factory values, keeping EEOffset[0].
  • Power cuts: done by hand: USB unplugged for about 60 s, after the RTC had passed midnight.
Build (ref) Test Result
dev fad0ffb7 boot on factory EEPROM RAM 35h/37h = 00/B4: bit 7 forced to 1 against the chip's 0. EEPROM still C0/10
dev fad0ffb7 RTC passes midnight RAM back to C0/10: switchover and trickle charger off, CLKOUT on
dev fad0ffb7 after midnight, ~60 s power cut time lost: the RTC came back at 2000-01-01, 0Eh = 11 (PORF = 1)
first version f0af008a (vendor Update sequence) store, midnight, reboot, ~60 s power cut EEPROM 40/34 written once. Held past midnight. No write on reboot. Time kept to the second (the RTC advanced 4:47 against 4:47 wall clock), 0Eh = 30 (BSF = 1, PORF = 0)
§3.15.6 sequence 4f6de862 store from factory, midnight, reboot, ~60 s power cut EEPROM 40/34 written once, RAM BSM restored. Held past midnight. No write on reboot. Time kept to the second (4:07 against 4:07), 0Eh = 30
6faac3bc store from factory, reboot, midnight 2 EEPROM bytes written, then 0 on reboot. Held past midnight
final logic 743bdae5 boot on an already-configured chip identified, nothing written: RAM/EEPROM 37h 34, Control 1 20 (EERD = 0), 0Eh 10 (BSF = 0)
final logic 743bdae5 rv factory, then reboot EEPROM and RAM 35h/37h C0/10 → 40/34. Control 1 20 (EERD = 0), 0Eh 10 (BSF cleared by the boot read)

This PR's 23b23b24 differs from 743bdae5 only by a comment. The power-cut retention rows ran on earlier commits of the same store sequence; what changed since is the identification gate, the BSF-bracketed boot read and the EEbusy gate, which decide whether the store runs, not what it stores. The chip's stored config, which is what keeps the time across a cut, was confirmed on 743bdae5 above.

Spurious switchovers: status 0Eh was polled every 30 s for 31 minutes on the f0af008a build, with DSM and the trickle charger stored, across 3 reboots. BSF never set. Spot reads across the session's serial-DFU flashes showed it set only after the deliberate power cuts.

Second implementation, same hardware. ZephCore carries the same §3.15.6 sequence, now upstream as liquidraver/ZephCore#98 (iteration and raw logs in ptr727/liquidraver-ZephCore#35). It was tested on a second RAK4631 + RAK12002, on USB only, and with the power again pulled by hand:

Build (ref) Test Result
dev f9d01cf, no RTC used 69 s unplug time lost (clock at 1970)
RTC declared, switchover not configured 72bfc7b 88 s unplug, chip at delivery EEPROM time lost: the RTC reset to 2000-01-01, 0Eh = 11 (PORF = 1, BSF = 0)
§3.15.6 sequence 334d9f7 store from delivery, reboot, midnight, 65 s unplug 35h/37h C0/10 → 40/34. Switchover off for 52.2 ms during the store and 14.3 ms on a reboot that wrote nothing. Held past an RTC midnight. Time kept, 0Eh = 30 (BSF = 1, PORF = 0)
earlier 7b6a82a about 55 min of 0Eh monitoring, then a 31 s unplug no spurious BSF; time kept

Fixes #3547

Iteration history and review: ptr727/meshcore-dev-MeshCore#11. Related: #3544, which stops begin() adopting a device that only ACKs at an RTC address, and #3421, which guards the RV3028 time reads and writes against the same switchover.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Preserve the original EERD state instead of unconditionally clearing it.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Persists RV3028 backup switchover and trickle-charger settings in EEPROM with validation, identity checks, and failure handling.

Changes:

  • Adds checked EEPROM read/write and refresh sequencing.
  • Preserves calibration bits and verifies configuration.
  • Adds bounded retries and RAM fallback.
  • Documents blocking configuration behavior.
File Summary
src/​helpers/​AutoDiscoverRTCClock.h Documents blocking configuration and retry behavior.
src/​helpers/​AutoDiscoverRTCClock.cpp Implements EEPROM configuration, verification, retries, and identity checks.

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

Comment thread src/helpers/AutoDiscoverRTCClock.cpp Outdated
@ptr727
ptr727 force-pushed the fix/rv3028-eeprom-config branch from 8469166 to aaf190d Compare October 4, 2026 15:58
@ptr727

ptr727 commented Oct 4, 2026

Copy link
Copy Markdown
Author

On the review overview's "Preserve the original EERD state instead of unconditionally clearing it": declining, deliberately.

  • The manual's EEPROM access procedure (RV-3028-C7 App Manual Rev 1.4, §4.6.7, p. 56) ends with "Clear EERD = 0 | It is recommended to enable Automatic EEPROM Refresh at the end of read/write access".
  • MeshCore never sets EERD itself. The only other code that touches this RTC uses initI2C, set24HourMode, setTime and the getters.
  • After a successful store, the closing Refresh has already copied the EEPROM over all of the configuration RAM (30h-37h). So nothing an earlier EERD = 1 was protecting survives either way, and keeping EERD = 1 would only switch off the daily refresh (§4.6.2, p. 54) that repairs a later-corrupted RAM byte.
  • An EERD = 1 left by other firmware doesn't survive a power-on reset anyway, because Control 1 resets to 00h (§3.7, p. 23).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

No unresolved blocking issues were identified.

Review effort: Lite
Findings: None

Resolved since last review (1)

AutoDiscoverRTCClock::begin() set 35h and 37h with plain register writes,
which change only the RAM mirror. With EERD = 0 the RV3028 reloads that
mirror from EEPROM every day at midnight (RV-3028-C7 Application Manual
Rev 1.4, 4.6.2 and 4.6.9), so on a part still holding the factory EEPROM
(BSM = 00, TCE = 0) the backup switchover and trickle charger turned off
at the first midnight after boot, and a later power cut lost the time.
Writing 0xB4 to 37h also forced bit 7, the LSB of the factory frequency
calibration.

Store the configuration in EEPROM instead, per the manual: EERD = 1, wait
for EEbusy, hold BSM = 00 in RAM while the EEPROM is accessed (3.15.6),
compare each byte with a single-byte EEPROM read and write only the bits
that differ (CLKOE in 35h; TCE, FEDE, BSM and TCR in 37h), then Refresh,
which restores the stored switchover mode, read the config back, and
clear EERD. The factory calibration bits are never written.

Nothing is written unless a read of the time registers shows no bit an
RV3028 always reads as 0 (3.2); a failed read writes nothing and is
retried, and only two reads that rule the device out, with BSF clear,
end it. The store also waits for a boot read bracketed by BSF, since the
EEPROM needs VDD (4.6.8). If EEbusy has not cleared, the switchover is
not re-enabled while an EEPROM operation may still be running (3.15.6,
4.6.7). Every I2C transfer is checked, since Melopero's
readFromRegister() returns 0xFF on failure. A failed store falls back to
the RAM mirror and is retried from getCurrentTime() up to 3 times per
boot.

Tested on a RAK4631 with a RAK12002: on dev the switchover and trickle
charger turned off at RTC midnight and a power cut lost the time; with
this change the config is stored once, survives midnight, and a power
cut keeps the time.

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

Copilot AI left a comment

Copy link
Copy Markdown

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

Address the retry path bypass for alternate RTC selection and avoid clearing EERD while EEPROM activity may still be in progress.

Review effort: Lite
Findings: None

@ptr727

ptr727 commented Oct 5, 2026

Copy link
Copy Markdown
Author

Replying to the Copilot overview on 23b23b24. It opened no threads, so both points are answered here. Neither leads to a code change. Links are to 23b23b24, and § and p. numbers are from the RV-3028-C7 Application Manual, Rev. 1.4, cited at the top of the description.

Retry path bypassed when another RTC is selected. That's right, and it's deliberate. getCurrentTime() returns the DS3231's time before it reaches the RV3028 retry, so on a board with both chips only the attempt in begin() runs. On such a board the RV3028 is never read or set, so its switchover and trickle settings decide nothing MeshCore does. If that first attempt fails, the chip falls back to the RAM-only config, as on dev. Running the retry there would write to a chip that isn't the clock, on every failed attempt. The PCF8563 and RX8130CE can't hit this, because the RV3028 comes before them in the selection order.

Clearing EERD while EEPROM activity may be in progress. This one is a judgement, and step 7 of the description states it: "Clearing EERD while EEbusy may still be set is a judgement, not documented behaviour (the §4.6.7 flowchart re-enables refresh after EEbusy = 0)". Clearing EERD starts no EEPROM operation. It only re-enables the automatic refresh, and the next one is at midnight (§4.6.2, p. 54). Leaving EERD set instead would stop every later automatic refresh for as long as the chip stays powered (§3.7, p. 23). What §3.15.6 (p. 39) does require during EEPROM activity is a disabled switchover, and that's kept: when EEbusy may still be set, the saved 37h isn't written back (L139-L142), and the RAM fallback waits for EEbusy = 0 (L244). liquidraver/ZephCore#98 and the Zephyr RV3028 driver handle EERD in the same order.

This branch has not been deployed

No deployments
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.

2 participants