From ad2df09dc5ae24a753ca9bd4990b5708adf3d2a8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 5 Oct 2026 08:21:54 -0700 Subject: [PATCH] rak4631: use a fitted RAK12002 RTC, and keep its backup switchover enabled 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 --- zephcore/Kconfig | 19 +- zephcore/adapters/clock/ZephyrRTCDiscover.c | 427 ++++++++++++++++-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 29 +- .../boards/nrf52840/rak4631/board.overlay | 27 ++ .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 27 ++ 5 files changed, 494 insertions(+), 35 deletions(-) diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 2bd353f..4f9cbf7 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -899,12 +899,19 @@ config ZEPHCORE_RTC_AUTODISCOVER help Probe I2C RTC chips declared with the "zephcore,rtc-i2c" binding (DS3231 / PCF8563 / RV3028 / RX8130CE). Boards with a battery/cap- - backed RTC opt in by including boards/common/rtc-i2c.dtsi; if a chip - holds a valid time it is restored at boot — shown tagged "L" until the - next external sync. On every GPS/app/CLI sync the time is written back - so it survives power-off. Compact raw-I2C reader (no Zephyr RTC - subsystem). Safe to leave on: a board with no rtc-i2c.dtsi include has - no RTC nodes and compiles to nothing. + backed RTC opt in by including boards/common/rtc-i2c.dtsi or + declaring their own node; if a chip holds a valid time it is + restored at boot — shown tagged "L" until the next external sync. + On every GPS/app/CLI sync the time is written back so it survives + power-off. A node with rv3028-eeprom-config has its time read and + written with the chip's backup switch flag cleared before and + checked after, and, when its chip is the one adopted, a read + identifies it, and a checked time read just before the store shows + no backup switchover, has that configuration committed to the + chip's EEPROM when it differs, with backup switchover held off + while the EEPROM is accessed. Compact raw-I2C reader (no Zephyr RTC + subsystem). Safe to leave on: a board that declares no + zephcore,rtc-i2c node compiles to nothing. menu "GPS Configuration" diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 3fb8184..59ecf13 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -28,6 +28,9 @@ LOG_MODULE_REGISTER(zephcore_rtc, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); /* Validity flag lives in the seconds byte itself (PCF8563 VL bit). */ #define RTC_STATUS_IN_SECONDS 0xFF +/* Only a board with an rv3028-eeprom-config node carries the RV3028 code. */ +#define RTC_RV3028_CFG DT_ANY_COMPAT_HAS_PROP_STATUS_OKAY(RTC_COMPAT, rv3028_eeprom_config) + struct rtc_desc { const struct device *bus; uint16_t addr; @@ -36,6 +39,10 @@ struct rtc_desc { uint8_t status_reg; /* power-loss flag register, or RTC_STATUS_IN_SECONDS */ 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 */ +#if RTC_RV3028_CFG + const uint8_t *cfg; /* rv3028-eeprom-config triplets, or NULL */ + uint8_t cfg_len; +#endif const char *name; }; @@ -50,6 +57,27 @@ struct rtc_desc { DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_ZERO_ARRAY) +#if RTC_RV3028_CFG +#define RTC_CFG_NAME(node) _CONCAT(rtc_cfg_, DT_DEP_ORD(node)) + +#define RTC_CFG_ARRAY(node) \ + IF_ENABLED(DT_NODE_HAS_PROP(node, rv3028_eeprom_config), \ + (static const uint8_t RTC_CFG_NAME(node)[] = \ + DT_PROP(node, rv3028_eeprom_config); \ + BUILD_ASSERT(sizeof(RTC_CFG_NAME(node)) % 3 == 0 && \ + sizeof(RTC_CFG_NAME(node)) <= UINT8_MAX, \ + "rv3028-eeprom-config is (register, mask, value) triplets");)) + +DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_CFG_ARRAY) + +#define RTC_CFG_FIELDS(node) \ + .cfg = COND_CODE_1(DT_NODE_HAS_PROP(node, rv3028_eeprom_config), \ + (RTC_CFG_NAME(node)), (NULL)), \ + .cfg_len = DT_PROP_LEN_OR(node, rv3028_eeprom_config, 0), +#else +#define RTC_CFG_FIELDS(node) +#endif + #define RTC_DESC_ENTRY(node) \ { \ .bus = DEVICE_DT_GET(DT_BUS(node)), \ @@ -61,6 +89,7 @@ DT_FOREACH_STATUS_OKAY(RTC_COMPAT, RTC_ZERO_ARRAY) .zero = COND_CODE_1( \ DT_NODE_HAS_PROP(node, zero_mask), \ (RTC_ZERO_NAME(node)), (NULL)), \ + RTC_CFG_FIELDS(node) \ .name = DT_NODE_FULL_NAME(node), \ }, @@ -198,6 +227,342 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) return RTC_FOUND; } +/* The 7-byte BCD time block for an epoch, in d's register order. */ +static void rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) +{ + int y; + unsigned m, day; + civil_from_days((int64_t)epoch / 86400, &y, &m, &day); + unsigned rem = epoch % 86400; + unsigned hour = rem / 3600; + unsigned min = (rem % 3600) / 60; + unsigned sec = rem % 60; + unsigned dow = (unsigned)(((epoch / 86400) + 4) % 7); /* 1970-01-01 = Thu */ + + blk[0] = BIN2BCD(sec); + blk[1] = BIN2BCD(min); + blk[2] = BIN2BCD(hour); + /* weekday occupies whichever of index 3/4 the date doesn't. */ + blk[d->date_index] = BIN2BCD(day); + blk[d->date_index == 4 ? 3 : 4] = (uint8_t)dow; + blk[5] = BIN2BCD(m); + blk[6] = BIN2BCD((unsigned)(y % 100)); +} + +/* Clear the power-loss flag (for chips whose flag is a separate reg; + * the seconds-bit chips clear it implicitly when we wrote sec above). */ +static void rtc_clear_power_flag(const struct rtc_desc *d) +{ + if (d->status_reg != RTC_STATUS_IN_SECONDS) { + uint8_t st; + if (i2c_reg_read_byte(d->bus, d->addr, d->status_reg, &st) == 0) { + (void)i2c_reg_write_byte(d->bus, d->addr, d->status_reg, + st & (uint8_t)~d->status_mask); + } + } +} + +#if RTC_RV3028_CFG + +/* RV3028 registers for reading and writing its configuration EEPROM. */ +#define RV3028_REG_STATUS 0x0E +#define RV3028_STATUS_EEBUSY BIT(7) +#define RV3028_STATUS_BSF BIT(5) +#define RV3028_REG_CONTROL1 0x0F +#define RV3028_CONTROL1_EERD BIT(3) +#define RV3028_REG_EE_ADDR 0x25 +#define RV3028_REG_EE_DATA 0x26 +#define RV3028_REG_EE_COMMAND 0x27 +#define RV3028_EE_CMD_REFRESH 0x12 /* all configuration EEPROM -> RAM */ +#define RV3028_EE_CMD_WRITE 0x21 /* EEDATA -> one EEPROM byte */ +#define RV3028_EE_CMD_READ 0x22 /* one EEPROM byte -> EEDATA */ +#define RV3028_REG_CFG_FIRST 0x35 /* EEPROM Clkout */ +#define RV3028_REG_CFG_LAST 0x37 /* EEPROM Backup */ +#define RV3028_BACKUP_BSM 0x0C /* 37h switchover mode; 00 = disabled */ + +/* Year 00h first and the real year last: a power loss, which no retry can + * repeat, cutting the write after the first byte and before the last leaves + * year 2000, so the next boot takes no time from it. */ +static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) +{ + return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00) == 0 && + i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 6) == 0 && + i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, blk[6]) == 0; +} + +/* Read or write the time block with BSF cleared before and checked after. A + * switchover to backup disables the chip's I2C mid-transfer (4.2): a read's + * tail comes back 0xFF, which inside the year byte can still be valid BCD, + * and a write's data integrity is no longer guaranteed (5.10). False unless + * the transfer is known to be whole. */ +static bool rv3028_steady(const struct rtc_desc *d, uint8_t blk[7], bool write) +{ + uint8_t st; + + return i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_STATUS, + RV3028_STATUS_BSF, 0) == 0 && + (write ? rv3028_write_time(d, blk) + : i2c_burst_read(d->bus, d->addr, d->time_reg, blk, 7) == 0) && + i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) == 0 && + !(st & RV3028_STATUS_BSF); +} + +/* Wait for EEbusy to clear: ~66 ms after power-on, ~16 ms for a byte write. + * A failed read ends the wait, rather than polling a dead bus through each + * transfer's timeout. */ +static bool rv3028_eeprom_idle(const struct rtc_desc *d) +{ + for (int ms = 0; ms <= 100; ms++) { + uint8_t st; + + if (i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) != 0) { + return false; + } + if (!(st & RV3028_STATUS_EEBUSY)) { + return true; + } + k_msleep(1); + } + return false; +} + +/* The manual (4.6.7): wait 10 ms after an EEPROM write, 1 ms after a read or + * Refresh, before checking EEbusy. */ +static bool rv3028_eeprom_cmd(const struct rtc_desc *d, uint8_t cmd, int32_t wait_ms) +{ + if (i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, 0x00) != 0 || + i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_COMMAND, cmd) != 0) { + return false; + } + k_msleep(wait_ms); + return rv3028_eeprom_idle(d); +} + +/* One EEPROM byte, read (4.6.6) or written (4.6.5). */ +static bool rv3028_eeprom_read(const struct rtc_desc *d, uint8_t reg, uint8_t *val) +{ + return i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_ADDR, reg) == 0 && + rv3028_eeprom_cmd(d, RV3028_EE_CMD_READ, 1) && + i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_EE_DATA, val) == 0; +} + +static bool rv3028_eeprom_write(const struct rtc_desc *d, uint8_t reg, uint8_t val) +{ + return i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_ADDR, reg) == 0 && + i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_EE_DATA, val) == 0 && + rv3028_eeprom_cmd(d, RV3028_EE_CMD_WRITE, 10); +} + +/* A config triplet is used only if its register is 35h-37h, its mask covers + * implemented bits only (35h bits 5:4 are not; manual 3.15.4), and no + * earlier triplet names the same register. */ +static bool rv3028_cfg_triplet_ok(const struct rtc_desc *d, size_t i) +{ + static const uint8_t impl[] = { 0xCF, 0xFF, 0xFF }; /* 35h, 36h, 37h */ + uint8_t reg = d->cfg[i]; + + if (reg < RV3028_REG_CFG_FIRST || reg > RV3028_REG_CFG_LAST || + (d->cfg[i + 1] & ~impl[reg - RV3028_REG_CFG_FIRST]) != 0) { + return false; + } + for (size_t k = 0; k < i; k += 3) { + if (d->cfg[k] == reg) { + return false; + } + } + return true; +} + +/* RAM 37h with the configured 37h bits applied, or as it was if none are. */ +static uint8_t rv3028_cfg_backup_ram(const struct rtc_desc *d, uint8_t ram) +{ + for (size_t i = 0; i + 2 < d->cfg_len; i += 3) { + if (d->cfg[i] == RV3028_REG_CFG_LAST && rv3028_cfg_triplet_ok(d, i)) { + return (ram & ~d->cfg[i + 1]) | (d->cfg[i + 2] & d->cfg[i + 1]); + } + } + return ram; +} + +/* Store the descriptor's rv3028-eeprom-config in the chip's EEPROM. The + * EEPROM is only touched with automatic refresh held off (4.6.7) and backup + * switchover disabled in RAM (3.15.6: BSM 00 for any EEPROM read or write). + * Each byte is compared against the EEPROM itself (4.6.6) and written only if + * it differs (4.6.5). A closing Refresh (4.6.4) reloads RAM from the EEPROM, + * switching back to the stored BSM, and the config is read back on every run. + * A triplet outside 35h-37h, masking an unimplemented bit, or repeating a + * register is ignored, so a devicetree mistake cannot write on every boot. + * Reports whether the config was confirmed, and if so whether EERD, which + * also stops the daily refresh, was cleared afterwards. */ +enum rv3028_store { RV3028_STORED, RV3028_EERD_SET, RV3028_NOT_STORED }; + +static enum rv3028_store rv3028_store_config(const struct rtc_desc *d) +{ + bool changed = false, held = false, refreshed = false, ok; + uint8_t backup = 0; + + ok = i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, + RV3028_CONTROL1_EERD, RV3028_CONTROL1_EERD) == 0 && + rv3028_eeprom_idle(d) && + i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, &backup) == 0; + if (ok) { + held = true; + ok = i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, + backup & ~RV3028_BACKUP_BSM) == 0; + } + + for (size_t i = 0; ok && i + 2 < d->cfg_len; i += 3) { + uint8_t old, reg = d->cfg[i], mask = d->cfg[i + 1]; + uint8_t val = d->cfg[i + 2] & mask; + + if (!rv3028_cfg_triplet_ok(d, i)) { + LOG_WRN("%s: config triplet for 0x%02x ignored", d->name, reg); + continue; + } + ok = rv3028_eeprom_read(d, reg, &old); + if (ok && (old & mask) != val) { + ok = rv3028_eeprom_write(d, reg, (old & ~mask) | val); + changed = true; + } + } + ok = refreshed = ok && rv3028_eeprom_cmd(d, RV3028_EE_CMD_REFRESH, 1); + for (size_t i = 0; ok && i + 2 < d->cfg_len; i += 3) { + uint8_t now; + + if (rv3028_cfg_triplet_ok(d, i)) { + ok = i2c_reg_read_byte(d->bus, d->addr, d->cfg[i], &now) == 0 && + ((now ^ d->cfg[i + 2]) & d->cfg[i + 1]) == 0; + } + } + + /* The Refresh did not run or did not finish, so RAM may still hold BSM + * 00. Write 37h as configured, RAM only, once EEbusy reads 0, never + * during an EEPROM operation (3.15.6). Clearing EERD below while + * EEbusy may still be set is our judgement: 4.6.7 only describes it + * after EEbusy is 0. */ + if (held && !refreshed && rv3028_eeprom_idle(d)) { + (void)i2c_reg_write_byte(d->bus, d->addr, RV3028_REG_CFG_LAST, + rv3028_cfg_backup_ram(d, backup)); + } + if (ok && changed) { + LOG_INF("%s: config committed to EEPROM", d->name); + } + if (i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_CONTROL1, + RV3028_CONTROL1_EERD, 0) != 0) { + return ok ? RV3028_EERD_SET : RV3028_NOT_STORED; + } + return ok ? RV3028_STORED : RV3028_NOT_STORED; +} + +/* Set the config in the RAM mirror only, which lasts until the next refresh. */ +static bool rv3028_set_ram(const struct rtc_desc *d) +{ + bool ok = true; + + for (size_t i = 0; i + 2 < d->cfg_len; i += 3) { + uint8_t old, reg = d->cfg[i], mask = d->cfg[i + 1]; + + if (rv3028_cfg_triplet_ok(d, i)) { + ok = i2c_reg_read_byte(d->bus, d->addr, reg, &old) == 0 && + i2c_reg_write_byte(d->bus, d->addr, reg, + (old & ~mask) | (d->cfg[i + 2] & mask)) == 0 && + ok; + } + } + return ok; +} + +/* A failed store is retried on the system work queue, a few times per boot + * only, since every attempt writes to the chip again. */ +#define RTC_CFG_RETRY K_MINUTES(10) +#define RTC_CFG_RETRIES 3 + +static uint8_t s_cfg_tries; +static void rv3028_cfg_retry_fn(struct k_work *work); +static K_WORK_DELAYABLE_DEFINE(s_cfg_retry, rv3028_cfg_retry_fn); + +/* Store the config, 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 chip still holding its delivery EEPROM turns switchover back off. */ +static void rv3028_configure(const struct rtc_desc *d) +{ + if (d->cfg == NULL) { + return; + } + s_cfg_tries++; + switch (rv3028_store_config(d)) { + case RV3028_STORED: + return; + case RV3028_EERD_SET: + LOG_WRN("%s: config stored, but EERD still set, so no daily refresh " + "(attempt %u)", d->name, s_cfg_tries); + break; + case RV3028_NOT_STORED: { + /* RAM only once EEbusy reads 0, as above. Not inside LOG_WRN: a + * build without logging does not evaluate its arguments. */ + bool set = rv3028_eeprom_idle(d) && rv3028_set_ram(d); + + LOG_WRN("%s: config not stored in EEPROM, %s in RAM (attempt %u)", d->name, + set ? "set" : "NOT set", s_cfg_tries); + break; + } + } + if (s_cfg_tries <= RTC_CFG_RETRIES) { + k_work_schedule(&s_cfg_retry, RTC_CFG_RETRY); + } +} + +/* Identify the chip again before each retry, since the store writes to it at + * once: configure on a clean read, stop if two reads rule it out, and try + * later on a failed read or a switchover. */ +static void rv3028_cfg_retry_fn(struct k_work *work) +{ + const struct rtc_desc *d = s_active; + uint8_t blk[7]; + enum rtc_verdict v = rtc_identify(d, blk); + + ARG_UNUSED(work); + if (v == RTC_FOUND && rv3028_steady(d, blk, false)) { + rv3028_configure(d); + } else if (v != RTC_NOT_THIS && ++s_cfg_tries <= RTC_CFG_RETRIES) { + k_work_schedule(&s_cfg_retry, RTC_CFG_RETRY); + } +} + +/* A time write that a switchover may have cut is repeated, with the time run + * on, until one is confirmed, for about a minute. For a power loss that takes + * the MCU down too, see rv3028_write_time()'s order. */ +#define RTC_SAVE_RETRY K_SECONDS(5) +#define RTC_SAVE_RETRIES 12 + +static uint32_t s_save_epoch; +static int64_t s_save_at; +static uint8_t s_save_tries; +static void rv3028_save_retry_fn(struct k_work *work); +static K_WORK_DELAYABLE_DEFINE(s_save_retry, rv3028_save_retry_fn); + +static void rv3028_save_retry_fn(struct k_work *work) +{ + uint8_t blk[7]; + + ARG_UNUSED(work); + rtc_time_block(s_active, s_save_epoch + + (uint32_t)((k_uptime_get() - s_save_at) / 1000), blk); + if (!rv3028_steady(s_active, blk, true)) { + if (++s_save_tries < RTC_SAVE_RETRIES) { + k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); + } else { + LOG_WRN("RTC %s: time write not confirmed after %u tries", + s_active->name, s_save_tries + 1U); + } + return; + } + rtc_clear_power_flag(s_active); + LOG_INF("RTC %s: time write confirmed on retry", s_active->name); +} + +#endif /* RTC_RV3028_CFG */ + /* Probe the chips in order; cache the first one found in s_active. Stop at * the first that holds a sane time, returned via epoch_out. */ static bool rtc_probe(uint32_t *epoch_out) @@ -221,8 +586,27 @@ static bool rtc_probe(uint32_t *epoch_out) continue; } +#if RTC_RV3028_CFG + /* Only a clean identification is configured: the store writes to + * the device at once. Nor right after a switchover: the EEPROM + * needs VDD (4.6.8). */ + bool clean = v == RTC_FOUND; + + if (clean && d->cfg != NULL && !rv3028_steady(d, blk, false)) { + v = RTC_FOUND_GARBLED; + clean = false; + } +#endif if (s_active == NULL) { s_active = d; /* RTC => our write-back target */ +#if RTC_RV3028_CFG + if (clean) { + rv3028_configure(d); + } else if (d->cfg != NULL) { + s_cfg_tries++; /* the skipped store is the first try */ + k_work_schedule(&s_cfg_retry, RTC_CFG_RETRY); + } +#endif } if (v == RTC_FOUND_GARBLED) { @@ -298,39 +682,28 @@ void zephcore_rtc_save(uint32_t epoch) } const struct rtc_desc *d = s_active; - int y; - unsigned m, day; - civil_from_days((int64_t)epoch / 86400, &y, &m, &day); - unsigned rem = epoch % 86400; - unsigned hour = rem / 3600; - unsigned min = (rem % 3600) / 60; - unsigned sec = rem % 60; - unsigned dow = (unsigned)(((epoch / 86400) + 4) % 7); /* 1970-01-01 = Thu */ - uint8_t blk[7]; - blk[0] = BIN2BCD(sec); - blk[1] = BIN2BCD(min); - blk[2] = BIN2BCD(hour); - /* weekday occupies whichever of index 3/4 the date doesn't. */ - blk[d->date_index] = BIN2BCD(day); - blk[d->date_index == 4 ? 3 : 4] = (uint8_t)dow; - blk[5] = BIN2BCD(m); - blk[6] = BIN2BCD((unsigned)(y % 100)); + rtc_time_block(d, epoch, blk); +#if RTC_RV3028_CFG + if (d->cfg != NULL) { + (void)k_work_cancel_delayable(&s_save_retry); + s_save_epoch = epoch; + s_save_at = k_uptime_get(); + s_save_tries = 0; + if (!rv3028_steady(d, blk, true)) { + LOG_WRN("RTC %s: time write not confirmed, repeating every 5 s", + d->name); + k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); + return; + } + } else +#endif if (i2c_burst_write(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) != 0) { LOG_WRN("RTC %s: time write failed", d->name); return; } - - /* Clear the power-loss flag (for chips whose flag is a separate reg; - * the seconds-bit chips clear it implicitly when we wrote sec above). */ - if (d->status_reg != RTC_STATUS_IN_SECONDS) { - uint8_t st; - if (i2c_reg_read_byte(d->bus, d->addr, d->status_reg, &st) == 0) { - (void)i2c_reg_write_byte(d->bus, d->addr, d->status_reg, - st & (uint8_t)~d->status_mask); - } - } + rtc_clear_power_flag(d); LOG_DBG("RTC %s: persisted time", d->name); } diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index ba82da2..c2d22ae 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -4,8 +4,9 @@ * Boot-time hardware-RTC auto-discovery (compact raw-I2C reader). * * Probes every I2C RTC chip declared with the "zephcore,rtc-i2c" binding - * (boards/common/rtc-i2c.dtsi, opt-in per board). Chips that aren't physically - * present fail the probe and are skipped — like the environment sensors. + * (boards/common/rtc-i2c.dtsi, or a board's own overlay). Chips that aren't + * physically present fail the probe and are skipped — like the environment + * sensors. * * If a present chip holds a valid time, zephcore_rtc_restore() returns it so * the soft clock can be seeded at boot (shown tagged "L" — local). Every @@ -36,6 +37,22 @@ extern "C" { * rules a device out. A time is taken only from a clean read whose fields, * hours and year are each in range (12-hour mode is not decoded). Returns * false if none present or no trustworthy time is held. + * + * At a descriptor with rv3028-eeprom-config, the time is read once more + * with the chip's backup switch flag (BSF) cleared before and checked + * after, and is not taken if the read failed or BSF was set. A chip adopted + * on a read that identified it (not on a failed second read), with BSF + * clear, has that config stored at once: after the power-on refresh (up to + * ~66 ms), backup switchover is turned off while the EEPROM is read, each + * byte that differs is written, and a refresh from EEPROM restores it. + * Measured on a RAK4631: about 14 ms when nothing is written, 52 ms for two + * bytes, then up to 2 ms for DSM to react (4.2.2). If the store fails, the + * config is set in RAM, which lasts until the chip's next refresh from + * EEPROM (daily while EERD is clear), and only once EEbusy reads 0; + * otherwise switchover stays as the store left it, off once the store has + * disabled it, until a retry or that refresh. A failed store, or one + * skipped at boot, is retried on the system work queue every 10 minutes, at + * most 3 times, each retry identifying the chip again first. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -46,6 +63,14 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * If restore adopted nothing but skipped a candidate whose first read was all * 0xFF, the first save probes again, once per boot, in the caller's context. * A device ruled out on its second read is not probed again for that reason. + * + * At a descriptor with rv3028-eeprom-config the time is written with BSF + * cleared before and checked after, year 00h first and the real year last, + * so a write cut by a power loss reads at boot as "time not yet set". A + * write not confirmed is repeated every 5 s on the system work queue, with + * the time run on, until one is confirmed, a newer save replaces it, or 12 + * repeats (13 attempts in all) have failed. Call only from the system work + * queue: the repeat shares state with this call. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index a0ada57..02ef9ed 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -215,6 +215,33 @@ }; #include "../../common/sensors-i2c.dtsi" + + /* + * A WisBlock slot can carry a RAK12002 (RV3028 at 0x52). Only that + * candidate, not all of rtc-i2c.dtsi: its DS3231 at 0x68 shares the + * address of WisBlock IMUs. + */ + rtc_rv3028: rtc-rv3028@52 { + compatible = "zephcore,rtc-i2c"; + reg = <0x52>; + time-reg = <0x00>; + date-index = <4>; + status-reg = <0x0e>; /* status */ + status-mask = <0x01>; /* PORF — power-on reset */ + /* 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 + * manual's rechargeable-backup circuit (7.3): direct switchover + * with trickle charging (0x37 TCE, BSM), TCR at its 3 kOhm delivery + * default, FEDE set as 3.15.6 says it always should be; CLKOUT off + * (0x35 CLKOE), as it is unused. 4.2.2 cautions + * that DSM can switch needlessly when VDD and VBACKUP are close; 7.3 + * prescribes it for a capacitor anyway, and level switching would + * leave the charge below 2.0 V unused. Any other RV3028 module at + * 0x52 gets the same settings, trickle charging included, kept in + * its EEPROM. */ + rv3028-eeprom-config = [35 80 00 37 3f 34]; + }; }; /* RAK4631 has BME680, not BME280 — override the common DTSI default */ diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 7e6643c..f241ecc 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -51,3 +51,30 @@ properties: while the power-loss flag does not read as set, on two reads, is not this RTC and is never adopted. Leave it out for a chip whose unused bits are not documented as reading 0. + + rv3028-eeprom-config: + type: uint8-array + description: > + RV3028 only. (register, mask, value) triplets for the chip's + EEPROM-backed configuration registers 0x35 (CLKOUT) to 0x37 + (backup switchover and trickle charger), one triplet per register, + masking implemented bits only (35h bits 5:4 are not); any other + triplet is ignored. When a read identifies the chip, it is + adopted, and no switchover showed during its time read, any + register whose masked bits differ is written to the chip's EEPROM, + one byte at a time with backup switchover held off, so the chip's + own refresh from EEPROM keeps it. If that fails, the config is set + in the RAM mirror once the chip's EEbusy flag reads 0, and lasts + until the chip's next refresh from EEPROM (daily while EERD is + clear); if EEbusy never clears, switchover stays as the store left + 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 + boot and written on each sync with the chip's backup switch flag + cleared before and checked after. A write sets year 00h first and + the real year last, so a write cut by a power loss reads as "time + not yet set", and a write not confirmed is repeated every 5 s, up + to 12 times. Set it only on a board whose backup supply is known: + trickle charging suits a supercapacitor or rechargeable cell, + never a primary cell.