Could NOT get this to work. Ultimately, I had to add a DS3231 to the I2C bus. I'm currently progging a Waveshare ESP32-P4 dev board (with a 7" screen). We tried the following things to try and get them to work:
ESP32-P4 RTC Time Persistence Across Power Cycles — Approaches Tried
Hardware
- Board: Waveshare ESP32-P4-WIFI6-Touch-LCD-7B
- SoC: ESP32-P4 rev 1.3
- ESP-IDF: v5.4
- VBAT: CR1220 coin cell, measured 3.27V, seated in onboard battery holder
- VBAT circuit: CR1220 connected to ESP_VBAT pin via onboard holder (confirmed in schematic — VBAT feeds the LP domain power input)
- Peripherals using SDIO: SD card on SDMMC Slot 0 (4-bit, 40MHz), WiFi via ESP32-C6 coprocessor on SDIO Slot 1 (esp_hosted, 4-bit, 40MHz)
Problem Statement
returns epoch 0 (1970-01-01) on every boot — including soft reboots from serial DTR/RTS reset — even though the ESP32-P4 datasheet (Section 4.1.4.10) documents a 48-bit RTC Timer that provides "uninterrupted operation during any reset or sleep mode, except for power-on reset of LP system."
The goal was to maintain wall-clock time across power outages using the CR1220 battery, without relying on NTP or deep sleep.
Relevant sdkconfig
Code: Select all
CONFIG_RTC_CLK_SRC_INT_RC=y
CONFIG_ESP_TIME_FUNCS_USE_RTC_TIMER=y
CONFIG_ESP_TIME_FUNCS_USE_ESP_TIMER=y
CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y
CONFIG_SOC_RTC_FAST_MEM_SUPPORTED=y
CONFIG_SOC_RTC_MEM_SUPPORTED=y
How ESP-IDF System Time Works (background)
computes:
- is stored in LP_STORE2/3 registers ( / ) via in
Code: Select all
components/newlib/port/esp_time_impl.c
- uses
Code: Select all
s_microseconds_offset + esp_system_get_time()
where bridges the ESP timer to the RTC counter
- writes: into LP_STORE2/3
- The 48-bit RTC counter () provides the monotonic reference
If LP_STORE2/3 and the RTC counter both survive a reset, time should persist.
[hr]
Approach 1: Baseline — System Calls Only (the P4 "onboard RTC" shim)
Implementation: Replaced the external DS3231 I2C driver with a shim that wraps
/
, treating the ESP32-P4's internal RTC as the time source.
Result: always returns epoch ~3 seconds (just boot time) after any reset. Diagnostic logging confirmed:
Code: Select all
[DIAG] Reset: POWERON, RTC counter: 3298603 us (0.0 hrs), epoch: 3
Even a serial DTR/RTS reset (not actual power loss) reports
and shows the RTC counter at ~3.3 seconds (just the time since boot started). The LP_STORE2/3 registers are zeroed.
Root cause: Every reset — including serial-triggered resets — appears as
to the ROM, which zeros LP_STORE registers and resets the 48-bit RTC counter. The VBAT switching that would keep the LP domain alive during main power loss is not enabled by default.
[hr]
Approach 2: NVS Bootstrap
Implementation: On every boot, before the RTC driver reads
, check if NVS contains
(minutes since epoch, saved on every NTP sync). If
shows year < 2025, call
with the NVS timestamp to prime the clock.
Also saved
(raw
value at last NTP sync) to NVS, so that if the RTC counter survived a power cycle, we could compute:
Code: Select all
current_time = nvs_epoch + (rtc_counter_now - rtc_counter_saved) / 1e6
for second-accurate recovery.
Result: Clock was primed to approximately correct time (minute resolution from NVS). Components started immediately without the 27-second NTP wait. However, the RTC counter delta was always zero because the counter resets on every boot — so the "second-accurate VBAT refinement" path never activated. The time was stale by the duration of the power outage.
[hr]
Approach 3: PMU VBAT Auto-Switch via Direct Register Write
Implementation: Based on Espressif's
ESP32-P4 VBAT documentation, set the PMU to auto-switch the LP domain to VBAT when main power drops:
Code: Select all
// In DS3231::init(), before any SDIO initialization:
REG_SET_FIELD(PMU_HP_SLEEP_LP_DIG_POWER_REG, PMU_HP_SLEEP_VDDBAT_MODE, 2); // AUTO mode
REG_SET_BIT(PMU_VDDBAT_CFG_REG, PMU_VDDBAT_SW_UPDATE);
// Poll for activation:
while (2 != REG_GET_FIELD(PMU_VDDBAT_CFG_REG, PMU_ANA_VDDBAT_MODE)) { ... }
Result — Partial success then crash:
The VBAT switching
did work for time persistence. After power cycling (unplug, wait, replug), the boot log showed:
Code: Select all
MAIN: RTC Time: 2026-04-03 12:16:33
This was the correct time, proving LP_STORE2/3 and the RTC counter survived the power cycle via VBAT.
However, both SDIO peripherals crashed on the VBAT-wake boot:
- SD card (SDMMC Slot 0):
Code: Select all
sdmmc_init_sd_ssr: sdmmc_send_cmd returned 0x107
→
- WiFi SDIO (Slot 1, esp_hosted):
Code: Select all
assert failed: xQueueSemaphoreTake queue.c:1709 (( pxQueue ))
Root cause: When VBAT keeps the LP domain powered, the reset reason is no longer
. In
Code: Select all
components/esp_system/port/soc/esp32p4/clk.c
lines 375-399, LP peripheral cleanup — including SDIO PLL clock gate clearing — only runs on
:
Code: Select all
if (rst_reason == RESET_REASON_CHIP_POWER_ON) {
// These only run on cold boot:
REG_CLR_BIT(LP_CLKRST_HP_CLK_CTRL_REG, LP_CLKRST_HP_SDIO_PLL2_CLK_EN);
REG_CLR_BIT(LP_CLKRST_HP_CLK_CTRL_REG, LP_CLKRST_HP_SDIO_PLL1_CLK_EN);
REG_CLR_BIT(LP_CLKRST_HP_CLK_CTRL_REG, LP_CLKRST_HP_SDIO_PLL0_CLK_EN);
// ... more LP clock cleanup ...
}
When VBAT keeps the LP domain alive, stale SDIO PLL clock states, LP UART clocks, and LP pad configurations survive into the next boot, causing the SDIO host controller to initialize against corrupted state.
Additional finding: (bits [1:0] of
) is a
read-only status register reflecting the current power mode. It only updates during actual PMU state transitions (sleep entry/exit), not from runtime register writes. The poll loop always timed out with "VBAT auto-switch failed to activate" even though the configuration register was written correctly — the mode only takes effect on the next power transition.
[hr]
Approach 4: LP Peripheral Force-Reset + SDIO Clock Cleanup + VBAT Enable
Implementation: Before enabling VBAT, force-reset LP peripherals and replay the
cleanup:
Code: Select all
// Step 1: Force-reset LP peripherals
PMU.power.lp_peri.force_reset = 1;
esp_rom_delay_us(100);
PMU.power.lp_peri.force_reset = 0;
// Step 2: Clear stale SDIO/LP clocks (from clk.c:375-399)
REG_CLR_BIT(LP_CLKRST_HP_CLK_CTRL_REG, LP_CLKRST_HP_SDIO_PLL2_CLK_EN);
REG_CLR_BIT(LP_CLKRST_HP_CLK_CTRL_REG, LP_CLKRST_HP_SDIO_PLL1_CLK_EN);
REG_CLR_BIT(LP_CLKRST_HP_CLK_CTRL_REG, LP_CLKRST_HP_SDIO_PLL0_CLK_EN);
// ... all other LP clock gates from clk.c ...
// Step 3: Enable VBAT
REG_SET_FIELD(PMU_HP_SLEEP_LP_DIG_POWER_REG, PMU_HP_SLEEP_VDDBAT_MODE, 2);
REG_SET_BIT(PMU_VDDBAT_CFG_REG, PMU_VDDBAT_SW_UPDATE);
Result — Worse crash:
The
force-reset was too destructive. It wiped LP_STORE registers (including
= XTAL frequency calibration) and corrupted LP ROM execution state:
Code: Select all
W (4868) rtc_clk: invalid RTC_XTAL_FREQ_REG value, assume 40MHz (×6 times)
After the first boot completed, subsequent boots crashed immediately in LP ROM code:
Code: Select all
rst:0x0 (N/A),boot:0xf (SPI_FAST_FLASH_BOOT)
invalid reset
Guru Meditation Error: Core 0 panic'ed (Illegal instruction)
PC: 0x4fc02ba6 ← LP ROM address space (0x4fc0xxxx)
Root cause: The
power domain encompasses more than just SDIO-related state. The force-reset pulse zeroed critical LP system registers (XTAL calibration in LP_STORE4, RTC slow clock calibration in LP_STORE1, boot_time in LP_STORE2/3) and corrupted LP ROM state, making the LP core execute garbage instructions on subsequent boots.
[hr]
Approach 5: RTC_NOINIT_ATTR (investigated, not attempted)
Finding: We investigated using
to store a
struct in LP RAM that survives resets. However:
- persistence tests are explicitly disabled for ESP32-P4 in ESP-IDF 5.4 due to issue IDF-9564:
Code: Select all
// components/esp_system/test_apps/.../test_reset_reason.c:27-29
#if (CONFIG_SOC_RTC_FAST_MEM_SUPPORTED || CONFIG_SOC_RTC_SLOW_MEM_SUPPORTED) && !CONFIG_IDF_TARGET_ESP32P4
#define CHECK_RTC_MEM 1
#endif
- The LP RAM linker section exists ( at , 8KB) and the macro is defined (not empty), but behavior is unvalidated on P4.
- Even if it worked, without the RTC counter surviving, we'd only have the epoch snapshot, not elapsed time.
[hr]
Summary of Findings
Code: Select all
Approach | RTC Counter? | LP_STORE? | SDIO? | Usable?
--------------------------|-------------|-----------|-----------|---------------------------
Baseline (no VBAT) | No (zero) | No (zero) | Yes | No time persistence
NVS bootstrap | N/A (zero) | N/A | Yes | Minute-res only, stale
VBAT auto-switch | YES | YES | NO crash | Time works, SDIO broken
LP force-reset + VBAT | Destroyed | Destroyed | Crashes | Unusable
RTC_NOINIT_ATTR | N/A (IDF-9564) | Untested/disabled on P4
[hr]
Questions for Espressif
1. Is there a supported way to enable VBAT power switching for the LP domain without going through the deep sleep infrastructure? Our device runs in full-power mode and experiences unexpected power loss. We need the PMU to be pre-configured so that when VDDA drops, it automatically switches to VBAT — without having called
first.
2. Can the SDIO PLL clock cleanup in
be extended to also run on VBAT wake resets? The current code only clears stale LP clocks on
. When VBAT keeps the LP domain alive, the reset reason changes and these cleanup steps are skipped, leaving SDIO in a broken state.
3. Is there a way to selectively reset LP peripherals (SDIO clocks, GPIO pads) without resetting the LP_STORE registers and RTC counter?
is too broad — it wipes everything including XTAL calibration and boot_time.
4. What is the status of IDF-9564 (
disabled on ESP32-P4)? If LP RAM persistence across resets were reliable, it could provide an alternative path for time persistence without VBAT.
5. The
component referenced in the ESP-IoT-Solution VBAT documentation — is there a timeline for it being integrated into ESP-IDF as a standard component? A proper API for VBAT management that handles the LP domain cleanup on wake would solve this cleanly.
[hr]
Resolution
We ultimately attached an external DS3231 I2C RTC module (SDA=GPIO7, SCL=GPIO8) with its own battery backup. This provides sub-second time accuracy across power cycles without any dependency on the ESP32-P4's LP domain persistence.