From 23b23b24be3e4ff034f2555befdb5caa1bcfd09c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 5 Oct 2026 08:43:20 -0700 Subject: [PATCH] Store RV3028 backup switchover and trickle charger config in EEPROM 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 --- src/helpers/AutoDiscoverRTCClock.cpp | 236 ++++++++++++++++++++++++++- src/helpers/AutoDiscoverRTCClock.h | 2 + 2 files changed, 236 insertions(+), 2 deletions(-) diff --git a/src/helpers/AutoDiscoverRTCClock.cpp b/src/helpers/AutoDiscoverRTCClock.cpp index af08fddb03..76d365011c 100644 --- a/src/helpers/AutoDiscoverRTCClock.cpp +++ b/src/helpers/AutoDiscoverRTCClock.cpp @@ -20,6 +20,233 @@ static bool rtc_8130_success = false; #define PCF8563_ADDRESS 0x51 #define RX8130CE_ADDRESS 0x32 +// RV3028 registers used to store its configuration in EEPROM (RV-3028-C7 Application Manual, 4.6) +#define RV3028_STATUS 0x0E // bit 7: EEbusy, bit 5: BSF +#define RV3028_CONTROL1 0x0F // bit 3: EERD, disables the automatic refresh from EEPROM +#define RV3028_EE_ADDR 0x25 +#define RV3028_EE_DATA 0x26 +#define RV3028_EE_COMMAND 0x27 +#define RV3028_EE_REFRESH 0x12 // copy all configuration EEPROM to RAM +#define RV3028_EE_WRITE 0x21 // write EE_DATA to one EEPROM byte +#define RV3028_EE_READ 0x22 // read one EEPROM byte into EE_DATA +#define RV3028_EE_CLKOUT 0x35 +#define RV3028_EE_BACKUP 0x37 +#define RV3028_BACKUP_BSM 0x0C // 37h switchover mode, 00 = disabled + +// Configuration MeshCore sets: { register, mask, value }. Bits outside the mask keep the chip's +// own values, notably 37h bit 7, the LSB of the factory frequency calibration. +static const uint8_t rv3028_config[][3] = { + { RV3028_EE_CLKOUT, 0x80, 0x00 }, // CLKOE = 0: CLKOUT pin off + // TCE = 1, FEDE = 1 (manual: "should always be set to 1"), BSM = 01 (DSM), TCR = 3 kOhm + { RV3028_EE_BACKUP, 0x3F, 0x34 }, +}; +#define RV3028_CONFIG_COUNT (sizeof(rv3028_config) / sizeof(rv3028_config[0])) + +// Melopero's readFromRegister() returns 0xFF when the I2C transfer fails, and its writes report +// nothing. A failed read must never be written back, least of all to the EEPROM, so these two +// helpers check every transfer. +static bool rv3028Read(uint8_t reg, uint8_t& val) { + TwoWire* wire = rtc_rv3028.i2c; + wire->beginTransmission(RV3028_ADDRESS); + wire->write(reg); + if (wire->endTransmission() != 0 || wire->requestFrom((uint8_t)RV3028_ADDRESS, (uint8_t)1) != 1) { + return false; + } + val = wire->read(); + return true; +} + +static bool rv3028Write(uint8_t reg, uint8_t val) { + TwoWire* wire = rtc_rv3028.i2c; + wire->beginTransmission(RV3028_ADDRESS); + wire->write(reg); + wire->write(val); + return wire->endTransmission() == 0; +} + +// Waits for EEbusy to clear: ~66 ms after the RTC powers on, ~16 ms for a byte write. A failed +// read ends the wait at once, rather than polling a dead bus through each transfer's timeout. +static bool rv3028EepromIdle() { + for (int i = 0; i < 100; i++) { + uint8_t status; + if (!rv3028Read(RV3028_STATUS, status)) return false; + if ((status & 0x80) == 0) return true; + delay(1); + } + return false; +} + +// Manual 4.6.7: wait 10 ms after an EEPROM write, 1 ms after a read or a Refresh, before +// checking EEbusy. delay() can return early on some cores: the extra 1 ms, with the bus time of +// the status read that follows, covers it. +static bool rv3028EepromCommand(uint8_t cmd, uint32_t wait_ms) { + if (!rv3028Write(RV3028_EE_COMMAND, 0x00) || !rv3028Write(RV3028_EE_COMMAND, cmd)) return false; + delay(wait_ms + 1); + return rv3028EepromIdle(); +} + +// One EEPROM byte, read (manual 4.6.6) or written (4.6.5) +static bool rv3028EepromRead(uint8_t reg, uint8_t& val) { + return rv3028Write(RV3028_EE_ADDR, reg) && rv3028EepromCommand(RV3028_EE_READ, 1) + && rv3028Read(RV3028_EE_DATA, val); +} + +static bool rv3028EepromWrite(uint8_t reg, uint8_t val) { + return rv3028Write(RV3028_EE_ADDR, reg) && rv3028Write(RV3028_EE_DATA, val) + && rv3028EepromCommand(RV3028_EE_WRITE, 10); +} + +// Stores rv3028_config in the RV3028's EEPROM. Setting only the RAM mirror does not last: while +// EERD = 0 the chip reloads it from EEPROM each day at midnight (manual 4.6.2, 4.6.9), which +// turns the switchover and trickle charger back off. The EEPROM is accessed with the automatic +// refresh held off (EERD = 1, 4.6.7) and the switchover disabled in RAM (3.15.6: BSM must be +// 00 or 10 for any EEPROM read or write). Each byte is compared against the EEPROM itself and +// written only if it differs, so the factory calibration in 36h is never rewritten. A closing +// Refresh reloads RAM from the EEPROM, which restores the switchover. Returns false if the +// configuration was not confirmed stored, or if EERD could not be cleared afterwards. +static bool rv3028StoreConfig() { + uint8_t ctrl1, backup; + if (!rv3028Read(RV3028_CONTROL1, ctrl1)) return false; + + // EERD = 1, wait for EEbusy = 0, then disable the switchover while the EEPROM is accessed + bool ok = rv3028Write(RV3028_CONTROL1, ctrl1 | 0x08) && rv3028EepromIdle() + && rv3028Read(RV3028_EE_BACKUP, backup); + bool held = ok; + ok = ok && rv3028Write(RV3028_EE_BACKUP, backup & ~RV3028_BACKUP_BSM); + + for (size_t i = 0; ok && i < RV3028_CONFIG_COUNT; i++) { + uint8_t reg = rv3028_config[i][0], mask = rv3028_config[i][1]; + uint8_t val = rv3028_config[i][2] & mask; + uint8_t old; + ok = rv3028EepromRead(reg, old); + if (ok && (old & mask) != val) ok = rv3028EepromWrite(reg, (old & ~mask) | val); + } + + // Refresh reloads RAM from the EEPROM, which also restores the switchover. The read-back runs + // even when nothing was written, so every boot confirms that the switchover came back. + bool refreshed = ok && rv3028EepromCommand(RV3028_EE_REFRESH, 1); + ok = refreshed; + for (size_t i = 0; ok && i < RV3028_CONFIG_COUNT; i++) { + uint8_t mask = rv3028_config[i][1], now; + ok = rv3028Read(rv3028_config[i][0], now) && (now & mask) == (rv3028_config[i][2] & mask); + } + + // If the Refresh did not run or finish, put the switchover back once EEbusy reads 0. A command + // whose write reported failure may still have been latched, so the check waits as long as + // after a write (4.6.7). While an EEPROM operation may still be running the switchover stays + // off, as 3.15.6 requires, until a later attempt succeeds; on a part whose EEPROM still holds + // the factory BSM = 00, a refresh alone does not bring it back. + if (held && !refreshed) { + delay(11); + if (rv3028EepromIdle()) rv3028Write(RV3028_EE_BACKUP, backup); + } + + // EERD = 0 once it may have been set. Read Control1 again first, since the chip clears TE itself + // when a single-shot countdown ends, so the earlier copy may be stale. If that read fails, + // Control1 is left alone and the store reports failure, for a retry to clear EERD if one is + // left. + return rv3028Read(RV3028_CONTROL1, ctrl1) && rv3028Write(RV3028_CONTROL1, ctrl1 & ~0x08) && ok; +} + +// Sets rv3028_config in the RAM mirror only, which holds until the next refresh. Returns false +// if any register could not be read or written. +static bool rv3028SetRam() { + bool ok = true; + for (size_t i = 0; i < RV3028_CONFIG_COUNT; i++) { + uint8_t reg = rv3028_config[i][0], mask = rv3028_config[i][1]; + uint8_t val = rv3028_config[i][2] & mask; + uint8_t old; + ok = rv3028Read(reg, old) && rv3028Write(reg, (old & ~mask) | val) && ok; + } + return ok; +} + +// Reads the time registers 00h-06h in one transfer: 1 if a register shows a bit that an RV3028 +// always reads as 0 (manual 3.2), as an EEPROM at 0x52 would, 0 if none does, -1 if the read +// failed +static int rv3028Check() { + static const uint8_t zero[7] = { 0x80, 0x80, 0xC0, 0xF8, 0xC0, 0xE0, 0x00 }; // 00h-06h + TwoWire* wire = rtc_rv3028.i2c; + wire->beginTransmission(RV3028_ADDRESS); + wire->write((uint8_t)0x00); + if (wire->endTransmission() != 0 || wire->requestFrom((uint8_t)RV3028_ADDRESS, (uint8_t)7) != 7) { + return -1; + } + int ruled_out = 0; + for (uint8_t i = 0; i < 7; i++) { + if (wire->read() & zero[i]) ruled_out = 1; + } + return ruled_out; +} + +// 1 if a read of 00h-06h reads like an RV3028, so its config may be written, 0 if two reads +// both show a bit an RV3028 always reads as 0, -1 if that is not settled. A second read is taken +// when the first rules the device out, so one corrupted read cannot hide a real RV3028. Two +// reads that rule it out count only if BSF reads 0: a switchover releases the bus (manual 4.2), +// which reads as 1s. Only those bits are checked, so a device at 0x52 that reads them as 0 +// still passes. +static int rv3028Identify() { + int r = rv3028Check(); + if (r == 1) r = rv3028Check(); + if (r == 0) return 1; + uint8_t status; + if (r == 1 && rv3028Read(RV3028_STATUS, status) && (status & 0x20) == 0) return 0; + return -1; +} + +// True if the device stays on VDD across a read of 00h-06h: BSF is cleared, which works only on +// VDD (manual 3.7), the time registers are read, and BSF must still read 0. A switchover in +// between means VDD is unstable, and the EEPROM needs VDD (4.6.8), so the store waits for a +// retry. BSF is cleared first, as a power cut before this boot leaves it set. +static bool rv3028OnVdd() { + uint8_t status; + return rv3028Read(RV3028_STATUS, status) && rv3028Write(RV3028_STATUS, status & ~0x20) + && rv3028Check() == 0 && rv3028Read(RV3028_STATUS, status) && (status & 0x20) == 0; +} + +// A failed configuration, or one skipped because a read failed, is retried from +// getCurrentTime(), a few times per boot only: a password-locked chip never succeeds, and every +// attempt writes to it again. +#define RV3028_RETRY_MS (10UL * 60 * 1000) +#define RV3028_RETRIES 3 + +static bool rv3028_pending = false; +static uint8_t rv3028_tries = 0; +static unsigned long rv3028_tried; + +// Stores the configuration once the device reads like an RV3028, falling back to the RAM mirror +// if that fails. Without the EEPROM store the RAM config lasts only until the next refresh, which +// on a part still holding the factory EEPROM turns the switchover back off. If the RAM fallback +// fails too, or EEbusy never reads 0, the RAM may still hold BSM = 00 from the store, with the +// switchover off until a later attempt succeeds. Either way it is retried while attempts +// remain. An identification that is not settled, or a switchover or failed transfer around the +// boot read, writes no config and is retried the same way. +static void rv3028Configure() { + rv3028_tried = millis(); + rv3028_tries++; + int id = rv3028Identify(); + if (id != 1) { + rv3028_pending = id < 0; + MESH_DEBUG_PRINTLN("RV3028: %s, config skipped (attempt %d)", + id < 0 ? "identification not settled" : "device at 0x52 is not an RV3028", + rv3028_tries); + return; + } + if (!rv3028OnVdd()) { + rv3028_pending = true; + MESH_DEBUG_PRINTLN("RV3028: switchover or failed transfer around the read, config deferred (attempt %d)", + rv3028_tries); + return; + } + rv3028_pending = !rv3028StoreConfig(); + if (rv3028_pending) { + // The switchover must stay off while an EEPROM operation may still be running (3.15.6) + bool ram = rv3028EepromIdle() && rv3028SetRam(); + MESH_DEBUG_PRINTLN("RV3028: config not stored in EEPROM, %s in RAM (attempt %d)", + ram ? "set" : "NOT set", rv3028_tries); + } +} + bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) { wire.beginTransmission(addr); uint8_t error = wire.endTransmission(); @@ -35,8 +262,9 @@ void AutoDiscoverRTCClock::begin(TwoWire& wire) { if (i2c_probe(wire, RV3028_ADDRESS)) { rtc_rv3028.initI2C(wire); - rtc_rv3028.writeToRegister(0x35, 0x00); - rtc_rv3028.writeToRegister(0x37, 0xB4); // Direct Switching Mode (DSM): when VDD < VBACKUP, switchover occurs from VDD to VBACKUP + // Store the backup switchover config: Direct Switching Mode (DSM), where the switchover to + // VBACKUP occurs when VDD < VBACKUP, with the trickle charger on + rv3028Configure(); rtc_rv3028.set24HourMode(); // Set the device to use the 24hour format (default) instead of the 12 hour format rv3028_success = true; } @@ -60,6 +288,10 @@ uint32_t AutoDiscoverRTCClock::getCurrentTime() { } if (rv3028_success) { + if (rv3028_pending && rv3028_tries <= RV3028_RETRIES + && millis() - rv3028_tried >= RV3028_RETRY_MS) { + rv3028Configure(); + } return DateTime( rtc_rv3028.getYear(), rtc_rv3028.getMonth(), diff --git a/src/helpers/AutoDiscoverRTCClock.h b/src/helpers/AutoDiscoverRTCClock.h index 11364cd811..177128a514 100644 --- a/src/helpers/AutoDiscoverRTCClock.h +++ b/src/helpers/AutoDiscoverRTCClock.h @@ -11,6 +11,8 @@ class AutoDiscoverRTCClock : public mesh::RTCClock { public: AutoDiscoverRTCClock(mesh::RTCClock& fallback) : _fallback(&fallback) { } + // begin() stores an RV3028's backup switchover config in its EEPROM, and getCurrentTime() + // retries that a few times if it failed, so either can block briefly on the RTC void begin(TwoWire& wire); uint32_t getCurrentTime() override; void setCurrentTime(uint32_t time) override;