From eb77c87421843404d58760f5846135691bc20de2 Mon Sep 17 00:00:00 2001 From: Juraj Sadel Date: Wed, 1 Jul 2026 13:25:20 +0200 Subject: [PATCH 1/8] regDMA experiments --- esp-hal/src/rtc_cntl/cpu_retention.rs | 585 +++++++++++++++++++++++ esp-hal/src/rtc_cntl/mod.rs | 11 + esp-hal/src/rtc_cntl/retention.rs | 544 +++++++++++++++++++++ esp-hal/src/rtc_cntl/sleep/esp32c6.rs | 128 ++++- qa-test/src/bin/sleep_timer_powerdown.rs | 151 ++++++ 5 files changed, 1409 insertions(+), 10 deletions(-) create mode 100644 esp-hal/src/rtc_cntl/cpu_retention.rs create mode 100644 esp-hal/src/rtc_cntl/retention.rs create mode 100644 qa-test/src/bin/sleep_timer_powerdown.rs diff --git a/esp-hal/src/rtc_cntl/cpu_retention.rs b/esp-hal/src/rtc_cntl/cpu_retention.rs new file mode 100644 index 00000000000..663667a2fbd --- /dev/null +++ b/esp-hal/src/rtc_cntl/cpu_retention.rs @@ -0,0 +1,585 @@ +//! # CPU power-down retention during light sleep (ESP32-C6) +//! +//! ## Overview +//! +//! During light sleep the ESP32-C6 can additionally power **down the CPU power +//! domain** (`pd_cpu`) while the rest of the digital system (the `TOP` domain: +//! RAM, peripherals, ...) stays powered. Powering the CPU down loses all of its +//! state, so before sleeping we save everything required to resume execution and +//! restore it on wakeup. +//! +//! Unlike peripheral (`TOP`-domain) retention, CPU retention does **not** use the +//! regDMA/PAU engine. The CPU register file and CSRs are not reachable by regDMA, +//! so ESP-IDF saves/restores them in **software**, which is exactly what this +//! module does. It mirrors `esp_sleep_cpu_retention()` in +//! `components/esp_hw_support/lowpower/port/esp32c6/sleep_cpu.c`. +//! +//! The save/restore is split into three parts, matching ESP-IDF: +//! +//! 1. **Critical registers** - the general-purpose registers and the handful of +//! machine CSRs needed to resume the interrupted control flow (`mepc`, +//! `mstatus`, `mtvec`, ...). Saved and restored in assembly +//! (`rv_core_critical_regs_save` / `rv_core_critical_regs_restore`), using a +//! `setjmp`/`longjmp`-style trick: the save routine records the return +//! context and, on wakeup, the ROM jumps to the restore routine which returns +//! *as if the save routine had just returned*. +//! 2. **Non-critical CSRs** - the rest of the architectural CSR state (PMP/PMA, +//! trigger module, performance counters, ...). Saved/restored in Rust via +//! `csrr`/`csrw`. +//! 3. **CPU-domain device registers** - memory-mapped registers that live in the +//! CPU power domain (interrupt matrix priority `INTPRI`, the `PLIC`/`CLINT` +//! interrupt controllers and the L1 cache control). Saved/restored with plain +//! loads/stores. +//! +//! ## Wakeup path +//! +//! The whole save -> sleep -> restore path runs from **internal RAM** (`.rwtext`, +//! i.e. IRAM). This is mandatory: when the CPU is powered back up the ROM jumps +//! directly to the wake-stub address we program into `LP_AON_STORE8` +//! (`RTC_SLEEP_WAKE_STUB_ADDR_REG`), and at that point the flash cache state has +//! been lost. Only after the cache configuration is restored may we touch flash +//! again, so every function on this path is annotated `#[ram]` and must avoid +//! calling into flash-resident code. +//! +//! References (ESP-IDF `v5.4`, commit +//! `8e27ea72c6688b79348b123ff40d556cfe16c8c3`, ESP32-C6): +//! - [`sleep_cpu.c`](https://github.com/espressif/esp-idf/blob/8e27ea72c6688b79348b123ff40d556cfe16c8c3/components/esp_hw_support/lowpower/port/esp32c6/sleep_cpu.c) +//! - [`sleep_cpu_asm.S`](https://github.com/espressif/esp-idf/blob/8e27ea72c6688b79348b123ff40d556cfe16c8c3/components/esp_hw_support/lowpower/port/esp32c6/sleep_cpu_asm.S) +//! - [`rvsleep-frames.h`](https://github.com/espressif/esp-idf/blob/8e27ea72c6688b79348b123ff40d556cfe16c8c3/components/esp_hw_support/lowpower/port/esp32c6/include/rvsleep-frames.h) + +use core::{ + ptr::addr_of_mut, + sync::atomic::{AtomicU32, Ordering}, +}; + +use procmacros::ram; + +use crate::peripherals::{LP_AON, PMU}; + +/// Number of times execution resumed through the ROM wake stub, i.e. how many +/// times the CPU power domain was actually powered down and restored. A sleep +/// that was rejected or where the CPU stayed powered does *not* increment this. +static CPU_POWERDOWN_WAKES: AtomicU32 = AtomicU32::new(0); + +/// Returns how many times the CPU power domain has actually been powered down +/// and successfully restored via the ROM wake stub. +/// +/// This is primarily a diagnostic: if it increases across light sleeps then the +/// CPU genuinely lost power (rather than the request being rejected or the CPU +/// merely clock-gated). +#[instability::unstable] +pub fn cpu_power_down_wake_count() -> u32 { + CPU_POWERDOWN_WAKES.load(Ordering::Relaxed) +} + +// --------------------------------------------------------------------------- +// Critical register frame (RvCoreCriticalSleepFrame) +// --------------------------------------------------------------------------- + +// The critical frame is a raw word buffer, not a typed struct: the assembly +// below is its only accessor and addresses every slot by byte offset +// (`RV_SLP_CTX_*`), so Rust just needs a correctly-sized, 4-byte-aligned buffer. +// The layout, word for word, matches ESP-IDF's `rvsleep-frames.h`: +// +// 0: mepc 1: ra 2: sp 3: gp 4: tp +// 5: t0 6: t1 7: t2 8: s0 9: s1 +// 10: a0 .. 17: a7 18: s2 .. 27: s11 28: t3 .. 31: t6 +// 32: mstatus 33: mtvec 34: mcause 35: mtval 36: mie 37: mip 38: pmufunc +const CRITICAL_FRAME_WORDS: usize = 39; + +/// Word index of the `pmufunc` slot (byte offset `RV_SLP_CTX_PMUFUNC` = 152). +/// `pmufunc & 0x3` encodes the phase: `1` = going to sleep, `3` = resumed via +/// the wake stub. +const PMUFUNC_WORD: usize = 38; + +/// Backing store for the critical frame. Lives in internal RAM (`.bss`), which +/// is retained while only the CPU domain is powered down. +static mut CRITICAL_FRAME: [u32; CRITICAL_FRAME_WORDS] = [0; CRITICAL_FRAME_WORDS]; + +/// Pointer the assembly reads to find [`CRITICAL_FRAME`]. Set before sleeping. +static mut RV_CORE_CRITICAL_REGS_FRAME: *mut u32 = core::ptr::null_mut(); + +unsafe extern "C" { + /// Save the CPU critical registers into `RV_CORE_CRITICAL_REGS_FRAME` and + /// mark the frame as "going to sleep". Returns the frame pointer. + fn rv_core_critical_regs_save() -> *mut u32; + /// Restore the CPU critical registers. Used as the ROM wake stub: on wakeup + /// it returns control as if [`rv_core_critical_regs_save`] had just returned. + fn rv_core_critical_regs_restore() -> *mut u32; +} + +// Ported from ESP-IDF's `rv_core_critical_regs_save` / `..._restore` in +// `sleep_cpu_asm.S`: +// https://github.com/espressif/esp-idf/blob/8e27ea72c6688b79348b123ff40d556cfe16c8c3/components/esp_hw_support/lowpower/port/esp32c6/sleep_cpu_asm.S +core::arch::global_asm!( + r#" + .set RV_SLP_CTX_MEPC, 0 + .set RV_SLP_CTX_RA, 4 + .set RV_SLP_CTX_SP, 8 + .set RV_SLP_CTX_GP, 12 + .set RV_SLP_CTX_TP, 16 + .set RV_SLP_CTX_T0, 20 + .set RV_SLP_CTX_T1, 24 + .set RV_SLP_CTX_T2, 28 + .set RV_SLP_CTX_S0, 32 + .set RV_SLP_CTX_S1, 36 + .set RV_SLP_CTX_A0, 40 + .set RV_SLP_CTX_A1, 44 + .set RV_SLP_CTX_A2, 48 + .set RV_SLP_CTX_A3, 52 + .set RV_SLP_CTX_A4, 56 + .set RV_SLP_CTX_A5, 60 + .set RV_SLP_CTX_A6, 64 + .set RV_SLP_CTX_A7, 68 + .set RV_SLP_CTX_S2, 72 + .set RV_SLP_CTX_S3, 76 + .set RV_SLP_CTX_S4, 80 + .set RV_SLP_CTX_S5, 84 + .set RV_SLP_CTX_S6, 88 + .set RV_SLP_CTX_S7, 92 + .set RV_SLP_CTX_S8, 96 + .set RV_SLP_CTX_S9, 100 + .set RV_SLP_CTX_S10, 104 + .set RV_SLP_CTX_S11, 108 + .set RV_SLP_CTX_T3, 112 + .set RV_SLP_CTX_T4, 116 + .set RV_SLP_CTX_T5, 120 + .set RV_SLP_CTX_T6, 124 + .set RV_SLP_CTX_MSTATUS, 128 + .set RV_SLP_CTX_MTVEC, 132 + .set RV_SLP_CTX_MCAUSE, 136 + .set RV_SLP_CTX_MTVAL, 140 + .set RV_SLP_CTX_MIE, 144 + .set RV_SLP_CTX_MIP, 148 + .set RV_SLP_CTX_PMUFUNC, 152 + + .section .rwtext, "ax" + .global rv_core_critical_regs_save + .type rv_core_critical_regs_save, @function + .align 4 +rv_core_critical_regs_save: + csrw mscratch, t0 # use mscratch as temp storage + la t0, {frame} + lw t0, 0(t0) # t0 = &CriticalFrame + + sw ra, RV_SLP_CTX_RA(t0) + sw sp, RV_SLP_CTX_SP(t0) + sw gp, RV_SLP_CTX_GP(t0) + sw tp, RV_SLP_CTX_TP(t0) + sw t1, RV_SLP_CTX_T1(t0) + sw t2, RV_SLP_CTX_T2(t0) + sw s0, RV_SLP_CTX_S0(t0) + sw s1, RV_SLP_CTX_S1(t0) + + # a0 is caller saved but is also the return value (frame pointer). + mv a0, t0 + sw a0, RV_SLP_CTX_A0(t0) + sw a1, RV_SLP_CTX_A1(t0) + sw a2, RV_SLP_CTX_A2(t0) + sw a3, RV_SLP_CTX_A3(t0) + sw a4, RV_SLP_CTX_A4(t0) + sw a5, RV_SLP_CTX_A5(t0) + sw a6, RV_SLP_CTX_A6(t0) + sw a7, RV_SLP_CTX_A7(t0) + sw s2, RV_SLP_CTX_S2(t0) + sw s3, RV_SLP_CTX_S3(t0) + sw s4, RV_SLP_CTX_S4(t0) + sw s5, RV_SLP_CTX_S5(t0) + sw s6, RV_SLP_CTX_S6(t0) + sw s7, RV_SLP_CTX_S7(t0) + sw s8, RV_SLP_CTX_S8(t0) + sw s9, RV_SLP_CTX_S9(t0) + sw s10, RV_SLP_CTX_S10(t0) + sw s11, RV_SLP_CTX_S11(t0) + sw t3, RV_SLP_CTX_T3(t0) + sw t4, RV_SLP_CTX_T4(t0) + sw t5, RV_SLP_CTX_T5(t0) + sw t6, RV_SLP_CTX_T6(t0) + + csrr t1, mstatus + sw t1, RV_SLP_CTX_MSTATUS(t0) + csrr t2, mtvec + sw t2, RV_SLP_CTX_MTVEC(t0) + csrr t3, mcause + sw t3, RV_SLP_CTX_MCAUSE(t0) + csrr t1, mtval + sw t1, RV_SLP_CTX_MTVAL(t0) + csrr t2, mie + sw t2, RV_SLP_CTX_MIE(t0) + csrr t3, mip + sw t3, RV_SLP_CTX_MIP(t0) + csrr t1, mepc + sw t1, RV_SLP_CTX_MEPC(t0) + + # pmufunc: clear low 2 bits, set bit0 => "going to sleep" (== 1) + li t1, 0xFFFFFFFC + lw t2, RV_SLP_CTX_PMUFUNC(t0) + and t2, t1, t2 + ori t2, t2, 0x1 + sw t2, RV_SLP_CTX_PMUFUNC(t0) + + mv t3, t0 + csrr t0, mscratch + lw t1, RV_SLP_CTX_T1(t3) + lw t2, RV_SLP_CTX_T2(t3) + lw t3, RV_SLP_CTX_T3(t3) + + ret + .size rv_core_critical_regs_save, . - rv_core_critical_regs_save + + .global rv_core_critical_regs_restore + .type rv_core_critical_regs_restore, @function + .align 4 +rv_core_critical_regs_restore: + la t0, {frame} + lw t0, 0(t0) # t0 = &CriticalFrame + beqz t0, 1f # never jump to a zero address + + # pmufunc: set low 2 bits => "awake" (== 3) + lw t1, RV_SLP_CTX_PMUFUNC(t0) + ori t1, t1, 0x3 + sw t1, RV_SLP_CTX_PMUFUNC(t0) + + lw t2, RV_SLP_CTX_MEPC(t0) + csrw mepc, t2 + lw t3, RV_SLP_CTX_MIP(t0) + csrw mip, t3 + lw t1, RV_SLP_CTX_MIE(t0) + csrw mie, t1 + lw t2, RV_SLP_CTX_MSTATUS(t0) + csrw mstatus, t2 + lw t3, RV_SLP_CTX_MTVEC(t0) + csrw mtvec, t3 + lw t1, RV_SLP_CTX_MCAUSE(t0) + csrw mcause, t1 + lw t2, RV_SLP_CTX_MTVAL(t0) + csrw mtval, t2 + + lw t6, RV_SLP_CTX_T6(t0) + lw t5, RV_SLP_CTX_T5(t0) + lw t4, RV_SLP_CTX_T4(t0) + lw t3, RV_SLP_CTX_T3(t0) + lw s11, RV_SLP_CTX_S11(t0) + lw s10, RV_SLP_CTX_S10(t0) + lw s9, RV_SLP_CTX_S9(t0) + lw s8, RV_SLP_CTX_S8(t0) + lw s7, RV_SLP_CTX_S7(t0) + lw s6, RV_SLP_CTX_S6(t0) + lw s5, RV_SLP_CTX_S5(t0) + lw s4, RV_SLP_CTX_S4(t0) + lw s3, RV_SLP_CTX_S3(t0) + lw s2, RV_SLP_CTX_S2(t0) + lw a7, RV_SLP_CTX_A7(t0) + lw a6, RV_SLP_CTX_A6(t0) + lw a5, RV_SLP_CTX_A5(t0) + lw a4, RV_SLP_CTX_A4(t0) + lw a3, RV_SLP_CTX_A3(t0) + lw a2, RV_SLP_CTX_A2(t0) + lw a1, RV_SLP_CTX_A1(t0) + lw a0, RV_SLP_CTX_A0(t0) + lw s1, RV_SLP_CTX_S1(t0) + lw s0, RV_SLP_CTX_S0(t0) + lw t2, RV_SLP_CTX_T2(t0) + lw t1, RV_SLP_CTX_T1(t0) + lw tp, RV_SLP_CTX_TP(t0) + lw gp, RV_SLP_CTX_GP(t0) + lw sp, RV_SLP_CTX_SP(t0) + lw ra, RV_SLP_CTX_RA(t0) + lw t0, RV_SLP_CTX_T0(t0) +1: + ret + .size rv_core_critical_regs_restore, . - rv_core_critical_regs_restore + "#, + frame = sym RV_CORE_CRITICAL_REGS_FRAME, +); + +// --------------------------------------------------------------------------- +// Non-critical CSRs (RvCoreNonCriticalSleepFrame) +// --------------------------------------------------------------------------- + +/// Read a CSR by numeric address (must be a compile-time constant). +#[inline(always)] +unsafe fn read_csr() -> u32 { + let value: u32; + unsafe { + core::arch::asm!("csrr {0}, {1}", out(reg) value, const CSR, options(nostack)); + } + value +} + +/// Write a CSR by numeric address (must be a compile-time constant). +#[inline(always)] +unsafe fn write_csr(value: u32) { + unsafe { + core::arch::asm!("csrw {1}, {0}", in(reg) value, const CSR, options(nostack)); + } +} + +/// Defines the set of non-critical CSRs to retain from a single canonical list, +/// generating the backing store plus the save and restore routines so the order +/// and slot count can never drift between them. +/// +/// The list and its order mirror `rv_core_noncritical_regs_save()` / +/// `..._restore()` in ESP-IDF's [`sleep_cpu.c`](https://github.com/espressif/esp-idf/blob/8e27ea72c6688b79348b123ff40d556cfe16c8c3/components/esp_hw_support/lowpower/port/esp32c6/sleep_cpu.c#L238-L401). +/// The `$name` tokens are documentation only; the CSR is addressed by number so +/// that the custom Espressif CSRs (`pmaaddr*`/`pmacfg*`, performance counters, +/// user GPIO) work without assembler support. +macro_rules! noncritical_csrs { + ($($name:ident = $csr:literal),+ $(,)?) => { + /// Backing store for the non-critical CSR values, one `u32` slot each. + static mut NONCRITICAL_FRAME: [u32; [$($csr),+].len()] = [0; [$($csr),+].len()]; + + #[ram] + fn save_noncritical() { + let buf = addr_of_mut!(NONCRITICAL_FRAME) as *mut u32; + let mut i = 0usize; + $( + unsafe { buf.add(i).write(read_csr::<$csr>()); } + i += 1; + )+ + let _ = i; + } + + #[ram] + fn restore_noncritical() { + let buf = addr_of_mut!(NONCRITICAL_FRAME) as *const u32; + let mut i = 0usize; + $( + unsafe { write_csr::<$csr>(buf.add(i).read()); } + i += 1; + )+ + let _ = i; + } + }; +} + +noncritical_csrs! { + mscratch = 0x340, + mideleg = 0x303, + misa = 0x301, + tselect = 0x7A0, + tdata1 = 0x7A1, + tdata2 = 0x7A2, + tcontrol = 0x7A5, + pmpaddr0 = 0x3B0, pmpaddr1 = 0x3B1, pmpaddr2 = 0x3B2, pmpaddr3 = 0x3B3, + pmpaddr4 = 0x3B4, pmpaddr5 = 0x3B5, pmpaddr6 = 0x3B6, pmpaddr7 = 0x3B7, + pmpaddr8 = 0x3B8, pmpaddr9 = 0x3B9, pmpaddr10 = 0x3BA, pmpaddr11 = 0x3BB, + pmpaddr12 = 0x3BC, pmpaddr13 = 0x3BD, pmpaddr14 = 0x3BE, pmpaddr15 = 0x3BF, + pmpcfg0 = 0x3A0, pmpcfg1 = 0x3A1, pmpcfg2 = 0x3A2, pmpcfg3 = 0x3A3, + pmaaddr0 = 0xBD0, pmaaddr1 = 0xBD1, pmaaddr2 = 0xBD2, pmaaddr3 = 0xBD3, + pmaaddr4 = 0xBD4, pmaaddr5 = 0xBD5, pmaaddr6 = 0xBD6, pmaaddr7 = 0xBD7, + pmaaddr8 = 0xBD8, pmaaddr9 = 0xBD9, pmaaddr10 = 0xBDA, pmaaddr11 = 0xBDB, + pmaaddr12 = 0xBDC, pmaaddr13 = 0xBDD, pmaaddr14 = 0xBDE, pmaaddr15 = 0xBDF, + pmacfg0 = 0xBC0, pmacfg1 = 0xBC1, pmacfg2 = 0xBC2, pmacfg3 = 0xBC3, + pmacfg4 = 0xBC4, pmacfg5 = 0xBC5, pmacfg6 = 0xBC6, pmacfg7 = 0xBC7, + pmacfg8 = 0xBC8, pmacfg9 = 0xBC9, pmacfg10 = 0xBCA, pmacfg11 = 0xBCB, + pmacfg12 = 0xBCC, pmacfg13 = 0xBCD, pmacfg14 = 0xBCE, pmacfg15 = 0xBCF, + utvec = 0x005, + ustatus = 0x000, + uepc = 0x041, + ucause = 0x042, + mpcer = 0x7E0, + mpcmr = 0x7E1, + mpccr = 0x7E2, + cpu_testbus_ctrl = 0x7E3, + upcer = 0x800, + upcmr = 0x801, + upccr = 0x802, + ugpio_oen = 0x803, + ugpio_in = 0x804, + ugpio_out = 0x805, +} + +// --------------------------------------------------------------------------- +// CPU-domain device registers (INTPRI / cache / PLIC / CLINT) +// --------------------------------------------------------------------------- + +/// A contiguous run of `words` 32-bit registers starting at `start`. +struct Region { + start: u32, + words: usize, +} + +/// Total number of 32-bit words covered by a set of [`Region`]s. Used to size +/// the backing stores so they always match the regions they hold. +const fn total_words(regions: &[Region]) -> usize { + let mut words = 0; + let mut i = 0; + while i < regions.len() { + words += regions[i].words; + i += 1; + } + words +} + +// Interrupt matrix priority registers (`INTPRI`, base 0x600C_5000). +const INTPRI_REGIONS: [Region; 2] = [ + // INTPRI_CORE0_CPU_INT_ENABLE_REG ..= INTPRI_RND_ECO_LOW_REG + Region { start: 0x600C_5000, words: 45 }, + // INTPRI_RND_ECO_HIGH_REG + Region { start: 0x600C_53FC, words: 1 }, +]; + +// L1 cache control (`EXTMEM`, base 0x600C_8000). +const CACHE_REGIONS: [Region; 2] = [ + // EXTMEM_L1_CACHE_CTRL_REG + Region { start: 0x600C_8004, words: 1 }, + // EXTMEM_L1_CACHE_WRAP_AROUND_CTRL_REG + Region { start: 0x600C_8020, words: 1 }, +]; + +// PLIC machine/user interrupt controllers (bases 0x2000_1000 / 0x2000_1400). +const PLIC_REGIONS: [Region; 4] = [ + // PLIC_MXINT_ENABLE_REG ..= PLIC_MXINT_CLAIM_REG + Region { start: 0x2000_1000, words: 38 }, + // PLIC_MXINT_CONF_REG + Region { start: 0x2000_13FC, words: 1 }, + // PLIC_UXINT_ENABLE_REG ..= PLIC_UXINT_CLAIM_REG + Region { start: 0x2000_1400, words: 38 }, + // PLIC_UXINT_CONF_REG + Region { start: 0x2000_17FC, words: 1 }, +]; + +// CLINT machine/user timers (bases 0x2000_1800 / 0x2000_1C00). +const CLINT_REGIONS: [Region; 2] = [ + // CLINT_MINT_SIP_REG ..= CLINT_MINT_MTIMECMP_H_REG + Region { start: 0x2000_1800, words: 6 }, + // CLINT_UINT_SIP_REG ..= CLINT_UINT_UTIMECMP_H_REG + Region { start: 0x2000_1C00, words: 6 }, +]; + +static mut INTPRI_FRAME: [u32; total_words(&INTPRI_REGIONS)] = [0; total_words(&INTPRI_REGIONS)]; +static mut CACHE_FRAME: [u32; total_words(&CACHE_REGIONS)] = [0; total_words(&CACHE_REGIONS)]; +static mut PLIC_FRAME: [u32; total_words(&PLIC_REGIONS)] = [0; total_words(&PLIC_REGIONS)]; +static mut CLINT_FRAME: [u32; total_words(&CLINT_REGIONS)] = [0; total_words(&CLINT_REGIONS)]; + +#[ram] +fn save_device_regs(regions: &[Region], buf: *mut u32) { + let mut out = buf; + for region in regions { + let mut addr = region.start as *const u32; + for _ in 0..region.words { + unsafe { + out.write(addr.read_volatile()); + out = out.add(1); + addr = addr.add(1); + } + } + } +} + +#[ram] +fn restore_device_regs(regions: &[Region], buf: *const u32) { + let mut src = buf; + for region in regions { + let mut addr = region.start as *mut u32; + for _ in 0..region.words { + unsafe { + addr.write_volatile(src.read()); + src = src.add(1); + addr = addr.add(1); + } + } + } +} + +// --------------------------------------------------------------------------- +// Entry: save -> sleep -> restore +// --------------------------------------------------------------------------- + +/// Read `mstatus` and clear its global machine-interrupt-enable bit (`MIE`), +/// returning the previous value. Mirrors `RV_READ_MSTATUS_AND_DISABLE_INTR()`. +#[inline(always)] +unsafe fn save_mstatus_and_disable_int() -> u32 { + let mstatus: u32; + unsafe { + core::arch::asm!("csrrci {0}, mstatus, 0b1000", out(reg) mstatus, options(nostack)); + } + mstatus +} + +#[inline(always)] +unsafe fn restore_mstatus(mstatus: u32) { + unsafe { + core::arch::asm!("csrw mstatus, {0}", in(reg) mstatus, options(nostack)); + } +} + +/// Save the CPU critical registers, program the wake stub, request sleep and +/// spin until the PMU reports wakeup (or rejects the request). +/// +/// Mirrors ESP-IDF's `do_cpu_retention()`: on the *save* pass `pmufunc & 0x3 == +/// 1`, so we set the wake stub and trigger sleep. On wakeup the ROM jumps to the +/// restore routine which returns here with `pmufunc & 0x3 == 3`, so we simply +/// fall through. +#[ram] +fn do_cpu_retention() { + let frame = unsafe { rv_core_critical_regs_save() }; + + let pmufunc = unsafe { frame.add(PMUFUNC_WORD).read_volatile() }; + if pmufunc & 0x3 == 0x1 { + // Going to sleep. + + // RTC_SLEEP_WAKE_STUB_ADDR_REG (= LP_AON_STORE8): where the ROM jumps + // to on light-sleep CPU-power-up. + LP_AON::regs() + .store8() + .write(|w| unsafe { w.bits(rv_core_critical_regs_restore as *const () as usize as u32) }); + + // pmu_ll_hp_set_sleep_enable + PMU::regs().slp_wakeup_cntl0().write(|w| w.sleep_req().bit(true)); + + // In the power-down case we never get past this loop: the CPU loses + // power here and resumes via the wake stub. If the sleep is rejected we + // fall out normally. + loop { + let int_raw = PMU::regs().int_raw().read(); + if int_raw.soc_wakeup().bit_is_set() || int_raw.soc_sleep_reject().bit_is_set() { + break; + } + } + } else if pmufunc & 0x3 == 0x3 { + // We resumed here via the ROM wake stub, which only happens after the + // CPU power domain was actually powered down and restored. + CPU_POWERDOWN_WAKES.fetch_add(1, Ordering::Relaxed); + } +} + +/// Perform a full CPU-power-down light sleep with software register retention. +/// +/// This is the equivalent of ESP-IDF's `esp_sleep_cpu_retention()`. The PMU sleep +/// configuration (wakeup/reject masks, power config, ...) must already have been +/// programmed by the caller; this function only adds the CPU save/restore around +/// the actual sleep trigger. +/// +/// # Safety +/// +/// Must be called with the PMU already configured for a `pd_cpu` light sleep and +/// with the system in a state where stopping the CPU is safe (interrupts are +/// disabled internally for the duration). +#[ram] +pub(crate) unsafe fn sleep_with_cpu_retention() { + unsafe { + RV_CORE_CRITICAL_REGS_FRAME = addr_of_mut!(CRITICAL_FRAME) as *mut u32; + + let mstatus = save_mstatus_and_disable_int(); + + save_device_regs(&PLIC_REGIONS, addr_of_mut!(PLIC_FRAME) as *mut u32); + save_device_regs(&CLINT_REGIONS, addr_of_mut!(CLINT_FRAME) as *mut u32); + save_device_regs(&INTPRI_REGIONS, addr_of_mut!(INTPRI_FRAME) as *mut u32); + save_device_regs(&CACHE_REGIONS, addr_of_mut!(CACHE_FRAME) as *mut u32); + save_noncritical(); + + do_cpu_retention(); + + // Restored in the reverse order of saving. The cache configuration must + // come back before we return to flash-resident code. + restore_noncritical(); + restore_device_regs(&CACHE_REGIONS, addr_of_mut!(CACHE_FRAME) as *const u32); + restore_device_regs(&INTPRI_REGIONS, addr_of_mut!(INTPRI_FRAME) as *const u32); + restore_device_regs(&CLINT_REGIONS, addr_of_mut!(CLINT_FRAME) as *const u32); + restore_device_regs(&PLIC_REGIONS, addr_of_mut!(PLIC_FRAME) as *const u32); + + restore_mstatus(mstatus); + } +} diff --git a/esp-hal/src/rtc_cntl/mod.rs b/esp-hal/src/rtc_cntl/mod.rs index 4c0bbdd44d4..80fbaa5c543 100644 --- a/esp-hal/src/rtc_cntl/mod.rs +++ b/esp-hal/src/rtc_cntl/mod.rs @@ -124,6 +124,17 @@ use crate::{peripherals::RTC_TIMER, system::Cpu, time::Duration}; #[cfg(sleep_driver_supported)] pub mod sleep; +// regDMA/PAU-based register retention of the TOP power domain's peripherals +// during light sleep. C6-only for now. Internal: driven automatically by the +// sleep path when `pd_top` is requested. +#[cfg(esp32c6)] +pub(crate) mod retention; + +// Software CPU-register retention for CPU power-down during light sleep. +// C6-only for now. +#[cfg(esp32c6)] +pub mod cpu_retention; + #[cfg_attr(esp32, path = "rtc/esp32.rs")] #[cfg_attr(esp32c2, path = "rtc/esp32c2.rs")] #[cfg_attr(esp32c3, path = "rtc/esp32c3.rs")] diff --git a/esp-hal/src/rtc_cntl/retention.rs b/esp-hal/src/rtc_cntl/retention.rs new file mode 100644 index 00000000000..d3b18ab50e9 --- /dev/null +++ b/esp-hal/src/rtc_cntl/retention.rs @@ -0,0 +1,544 @@ +//! # Register DMA (regDMA) based register retention +//! +//! ## Overview +//! +//! ESP32-C6 contains a **Power Assist Unit (PAU)** with a **regDMA** engine that +//! can automatically back up and restore peripheral/CPU register state to and +//! from RAM. ESP-IDF uses this engine to retain register contents while a power +//! domain (e.g. the CPU or the digital `TOP` domain) is powered down during +//! light sleep, so that execution can resume seamlessly after wakeup. +//! +//! regDMA walks a linked list of *nodes* stored in RAM. Each node describes one +//! backup/restore operation ([`RegdmaLink`]): CONTINUOUS (a run of registers via +//! a RAM buffer), ADDR_MAP (a run of registers where a bitmap selects which ones +//! to transfer), WRITE (a masked register write) or WAIT (poll a register). +//! +//! The list is executed by the **PMU auto-trigger** over PAU entry link 0: when +//! the digital `TOP` domain is powered down during light sleep the PMU runs the +//! list to back up the registers on the way into sleep and restore them on +//! wakeup, with no CPU involvement. The `sys_periph` module builds the +//! TOP-domain register set to retain, and [`enable_top_retention`] arms it. +//! +//! References (ESP-IDF `v5.4`): +//! - `components/soc/include/soc/regdma.h` (node layout) +//! - `components/hal/esp32c6/include/hal/pau_ll.h` +//! - `components/hal/esp32c6/pau_hal.c` +//! - `components/esp_hw_support/port/pau_regdma.c` + +use core::sync::atomic::{Ordering, fence}; + +use crate::peripherals::{PAU, PCR, PMU}; + +// Bit layout of `regdma_link_head_t` (see ESP-IDF `regdma.h`): +// https://github.com/espressif/esp-idf/blob/v5.4/components/soc/include/soc/regdma.h#L114-L123 +const HEAD_LENGTH_MASK: u32 = 0x3ff; // bits 0..=9: register count (words) +const HEAD_MODE_SHIFT: u32 = 16; // bits 16..=19: link mode +const HEAD_SKIP_R_BIT: u32 = 1 << 29; // skip this node on restore +const HEAD_SKIP_B_BIT: u32 = 1 << 30; // skip this node on backup +const HEAD_EOF_BIT: u32 = 1 << 31; // end of link + +/// regDMA link node mode (`regdma_link_mode_t`). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(u32)] +enum LinkMode { + /// Back up/restore a run of consecutive registers via a RAM buffer. + Continuous = 0, + /// Back up/restore a run of registers via a RAM buffer, where a 4-word + /// bitmap selects which registers in the window are actually transferred + /// (skipping e.g. read-only status/FIFO registers interspersed in a block). + AddrMap = 1, + /// Unconditionally write a masked value to a register. + Write = 2, + /// Poll a register until `(reg & mask) == value`. + Wait = 3, +} + +/// A single regDMA linked-list node. +/// +/// The in-memory layout must match what the PAU hardware expects: the hardware +/// link address points at the `head` field, followed by the four body words. +/// The software-only `stat` block that ESP-IDF keeps *before* `head` is not +/// needed here, so it is omitted. +/// +/// The CONTINUOUS and WRITE/WAIT node bodies are both four words; the ADDR_MAP +/// body adds a four-word register-selection bitmap. A single struct with a +/// trailing `map` array covers all four modes (the hardware reads only as many +/// body words as the mode requires and then follows `next`, so the unused +/// trailing words are harmless padding for the other modes). Branch nodes are +/// not implemented. +/// +/// - CONTINUOUS: `w0 = backup addr`, `w1 = restore addr`, `w2 = RAM buffer`. +/// - ADDR_MAP: as CONTINUOUS, plus `map` selecting which registers to transfer. +/// - WRITE/WAIT: `w0 = target addr`, `w1 = value`, `w2 = mask` (`mem`/`map` +/// unused). +#[repr(C, align(4))] +#[derive(Clone, Copy)] +pub(crate) struct RegdmaLink { + /// Packed `regdma_link_head_t`. + head: u32, + /// Pointer to the next node's `head`, or `0` for the end of the list. + next: u32, + w0: u32, + w1: u32, + w2: u32, + /// ADDR_MAP register-selection bitmap; zero (and unread) for other modes. + map: [u32; 4], +} + +impl RegdmaLink { + const EMPTY: Self = Self { + head: 0, + next: 0, + w0: 0, + w1: 0, + w2: 0, + map: [0; 4], + }; + + fn head(mode: LinkMode, len: u32, skip_b: bool, skip_r: bool) -> u32 { + let mut head = (len & HEAD_LENGTH_MASK) | ((mode as u32) << HEAD_MODE_SHIFT) | HEAD_EOF_BIT; + if skip_b { + head |= HEAD_SKIP_B_BIT; + } + if skip_r { + head |= HEAD_SKIP_R_BIT; + } + head + } + + /// A CONTINUOUS node backing up/restoring `len` words at `reg` via `storage`. + fn continuous(reg: u32, storage: u32, len: u32) -> Self { + Self::continuous_split(reg, reg, storage, len) + } + + /// A CONTINUOUS node whose backup source (`backup`) and restore destination + /// (`restore`) registers differ, sharing one RAM buffer. Used where hardware + /// exposes a value register for readback and a separate load register for + /// restore (e.g. the SysTimer counter). + fn continuous_split(backup: u32, restore: u32, storage: u32, len: u32) -> Self { + Self { + head: Self::head(LinkMode::Continuous, len, false, false), + next: 0, + w0: backup, + w1: restore, + w2: storage, + map: [0; 4], + } + } + + /// An ADDR_MAP node backing up/restoring the `count` registers selected by + /// `map` from the register window starting at `reg`, via `storage`. + /// + /// `map` is a bitmap over the window (bit `i` = the register at + /// `reg + i * 4`); the engine transfers `count` registers total (one per + /// set bit, walking bits from LSB up) into `count` consecutive words of + /// `storage`. Used to skip read-only/FIFO registers interspersed in a + /// peripheral's register block (e.g. the console UART). + fn addr_map(reg: u32, storage: u32, count: u32, map: [u32; 4]) -> Self { + Self { + head: Self::head(LinkMode::AddrMap, count, false, false), + next: 0, + w0: reg, + w1: reg, + w2: storage, + map, + } + } + + /// A WRITE node that writes `value` (under `mask`) to `target`. + /// + /// `skip_b`/`skip_r` select whether the write happens during backup and/or + /// restore (WRITE/WAIT nodes are usually restore-only or backup-only). + fn write(target: u32, value: u32, mask: u32, skip_b: bool, skip_r: bool) -> Self { + Self { + head: Self::head(LinkMode::Write, 0, skip_b, skip_r), + next: 0, + w0: target, + w1: value, + w2: mask, + map: [0; 4], + } + } + + /// A WAIT node that polls `target` until `(reg & mask) == value`. + fn wait(target: u32, value: u32, mask: u32, skip_b: bool, skip_r: bool) -> Self { + Self { + head: Self::head(LinkMode::Wait, 0, skip_b, skip_r), + next: 0, + w0: target, + w1: value, + w2: mask, + map: [0; 4], + } + } + + fn addr(&self) -> u32 { + core::ptr::addr_of!(self.head) as u32 + } +} + +/// Chain a slice of nodes into a single linked list: each node's `next` points +/// at the following node's `head`, and only the last node keeps its EOF flag. +/// Returns the head address to program into the PAU. +fn link_nodes(nodes: &mut [RegdmaLink]) -> u32 { + let len = nodes.len(); + for i in 0..len { + if i + 1 < len { + nodes[i].next = nodes[i + 1].addr(); + nodes[i].head &= !HEAD_EOF_BIT; + } else { + nodes[i].next = 0; + nodes[i].head |= HEAD_EOF_BIT; + } + } + nodes[0].addr() +} + +/// Arm PMU-driven regDMA retention of the TOP-domain system peripherals for the +/// upcoming light sleep. +/// +/// When the PMU powers down the digital `TOP` domain (`pd_top`) it loses the +/// system-peripheral register state, so it must be backed up on the +/// HP_ACTIVE -> HP_SLEEP transition and restored on HP_SLEEP -> HP_ACTIVE. Both +/// transitions run PAU entry link 0 (the direction is chosen by the PMU), so a +/// single combined list (see the `sys_periph` module) serves both. +/// +/// This programs the entry-link address and enables the two backup phases. The +/// backup *mode*/direction and clocks are already configured per HP state by the +/// sleep power config; only the enable bits (reset every sleep) are flipped +/// here. Mirrors ESP-IDF `sleep_retention` link setup + +/// `pmu_sleep_enable_regdma_backup()` (active/sleep phases only, as there is no +/// modem state). +/// +/// Must be called after the PMU power config has been applied (which rewrites +/// the backup registers) and before the sleep request. +pub(crate) fn enable_top_retention() { + // pau_ll_enable_bus_clock(true): enable the regDMA bus clock and release its + // reset before programming the entry link. + PCR::regs().regdma_conf().modify(|_, w| { + w.regdma_clk_en().set_bit(); + w.regdma_rst_en().clear_bit() + }); + // pau_hal_set_regdma_wait_timeout: bound how long a WAIT node polls a + // register so a never-satisfied condition can't hang the engine. Values + // match ESP-IDF's PAU_REGDMA_LINK_WAIT_{RETRY_COUNT,READ_INTERNAL}. + PAU::regs().regdma_bkp_conf().modify(|_, w| unsafe { + w.link_tout_thres().bits(1000); + w.read_interval().bits(32) + }); + + // Build the combined SYS_PERIPH list and program it as PAU entry link 0. + let head = link_nodes(sys_periph::build_link()); + fence(Ordering::SeqCst); + PAU::regs() + .regdma_link_0_addr() + .write(|w| unsafe { w.bits(head) }); + + // pmu_sleep_enable_regdma_backup (active <-> sleep only): back up on + // active->sleep, restore on sleep->active. + let pmu = PMU::regs(); + pmu.hp_sleep_backup() + .modify(|_, w| w.hp_active2sleep_backup_en().set_bit()); + pmu.hp_active_backup() + .modify(|_, w| w.hp_sleep2active_backup_en().set_bit()); +} + +/// ESP32-C6 TOP-domain system-peripheral retention link. +/// +/// When the digital `TOP` power domain is powered down during light sleep, the +/// registers of the core system peripherals are lost and must be regDMA-backed +/// up beforehand and restored on wakeup. This module builds the linked list +/// describing those register regions. +/// +/// The set and ordering mirror ESP-IDF's `SLEEP_RETENTION_MODULE_SYS_PERIPH` +/// plus `SLEEP_RETENTION_MODULE_CLOCK_SYSTEM` (both TOP-domain), sorted by the +/// same retention priority (system clock first). Within a link, ESP-IDF keeps +/// the same nodes for backup (entry 0) and restore (entry 2), so a single +/// combined list can be programmed into both PAU entry links. +/// +/// References (ESP-IDF `v5.4`): +/// - [`system_retention_periph.c`](https://github.com/espressif/esp-idf/blob/v5.4/components/soc/esp32c6/system_retention_periph.c) +/// - [`sleep_clock.c`](https://github.com/espressif/esp-idf/blob/v5.4/components/esp_hw_support/lowpower/port/esp32c6/sleep_clock.c) +/// - [`sleep_system_peripheral.c`](https://github.com/espressif/esp-idf/blob/v5.4/components/esp_hw_support/sleep_system_peripheral.c) +mod sys_periph { + use super::{HEAD_LENGTH_MASK, RegdmaLink}; + + /// TEE mode-control register, rewritten early on restore to unlock access. + const TEE_M4_MODE_CTRL_REG: u32 = 0x6009_8010; + + /// A run of `count` consecutive 32-bit registers starting at `base`. + struct ContRegion { + base: u32, + count: u32, + } + + /// Continuous register regions to retain, in ESP-IDF retention-priority + /// order (highest priority first). The end registers used to size each + /// region are noted; counts are `((end - base) / 4) + 1`. + const CONT_REGIONS: &[ContRegion] = &[ + // PRI_0 - system clock/reset (PCR) + ContRegion { base: 0x6009_6000, count: 79 }, // PCR base ..= PCR_SRAM_POWER_CONF_REG (+0x138) + ContRegion { base: 0x6009_6FF0, count: 1 }, // PCR_RESET_EVENT_BYPASS_REG + // PRI_4 - TEE/APM + ContRegion { base: 0x6009_9000, count: 68 }, // HP_APM base ..= HP_APM_CLOCK_GATE_REG (+0x10c) + ContRegion { base: 0x6009_8000, count: 33 }, // TEE base ..= TEE_CLOCK_GATE_REG (+0x80) + // PRI_5 - interrupt matrix + HP system + ContRegion { base: 0x6001_0000, count: 81 }, // INTMTX base ..= INTMTX_CORE0_CLOCK_GATE_REG (+0x140) + ContRegion { base: 0x6009_5000, count: 18 }, // HP_SYSTEM base ..= HP_SYSTEM_MEM_TEST_CONF_REG (+0x44) + // PRI_6 - IO MUX + GPIO matrix + ContRegion { base: 0x6009_0000, count: 32 }, // IO_MUX base ..= IO_MUX_GPIO30_REG (+0x7c) + ContRegion { base: 0x6009_1554, count: 35 }, // GPIO_FUNC0_OUT_SEL ..= GPIO_FUNC34_OUT_SEL + ContRegion { base: 0x6009_114C, count: 127 }, // GPIO_STATUS_NEXT ..= GPIO_FUNC124_IN_SEL + ContRegion { base: 0x6009_1000, count: 64 }, // GPIO base ..= GPIO_PIN34_REG (+0xfc) + // PRI_6 - Flash SPI mem (SPIMEM1 then SPIMEM0). MMU content/index + // registers are intentionally excluded (see ESP-IDF note). + ContRegion { base: 0x6000_3000, count: 55 }, // SPIMEM1 base ..= SPI_MEM_SPI_SMEM_DDR (+0xd8) + ContRegion { base: 0x6000_3100, count: 41 }, // SPIMEM1 FMEM_PMS0_ATTR ..= SMEM_AC (+0x1a0) + ContRegion { base: 0x6000_3200, count: 1 }, // SPIMEM1 CLOCK_GATE + ContRegion { base: 0x6000_3384, count: 31 }, // SPIMEM1 MMU_POWER_CTRL ..= DATE (+0x3fc) + ContRegion { base: 0x6000_2000, count: 55 }, // SPIMEM0 base ..= SPI_MEM_SPI_SMEM_DDR + ContRegion { base: 0x6000_2100, count: 41 }, // SPIMEM0 FMEM_PMS0_ATTR ..= SMEM_AC + ContRegion { base: 0x6000_2200, count: 1 }, // SPIMEM0 CLOCK_GATE + ContRegion { base: 0x6000_2384, count: 31 }, // SPIMEM0 MMU_POWER_CTRL ..= DATE + ]; + + /// Index in [`CONT_REGIONS`] at which the TEE/APM (PRI_4) group starts; the + /// PRI_2 TEE-critical WRITE node is inserted just before it. + const TEE_APM_START: usize = 2; + + /// Index in [`CONT_REGIONS`] at which the IO MUX / GPIO (PRI_6) group + /// starts. The console-UART (PRI_5) nodes are inserted just before it, so + /// the continuous regions split into a PRI_4/5 prefix (TEE/APM, interrupt + /// matrix, HP system) and a PRI_6 suffix (IO MUX, GPIO, SPI mem). + const IOMUX_START: usize = 6; + + const fn total_words() -> usize { + let mut words = 0; + let mut i = 0; + while i < CONT_REGIONS.len() { + words += CONT_REGIONS[i].count as usize; + i += 1; + } + words + } + + // Console UART0 (base 0x6000_0000). Retained via an ADDR_MAP node whose + // bitmap selects the 21 configuration registers out of the 37-register + // window between UART_INT_ENA_REG (+0x0c) and UART_ID_REG (+0x9c), skipping + // the interspersed FIFO/status/interrupt-raw registers, followed by a + // restore-only WRITE+WAIT that pulses UART_REG_UPDATE to load the shadow + // (`_SYNC`) registers. Values from ESP-IDF v5.4 `uart_periph.c` + // `UART_SLEEP_RETENTION_ENTRIES` and `uart_reg.h`. + const UART_INT_ENA_REG: u32 = 0x6000_000C; + const UART_REG_UPDATE_REG: u32 = 0x6000_0098; + const UART_REG_UPDATE: u32 = 1 << 0; + /// Number of registers actually retained (set bits in `UART_REGS_MAP`). + const UART_RETENTION_REGS_CNT: u32 = 21; + /// `uart_regs_map[4]` from ESP-IDF: bitmap over the INT_ENA..ID window. + const UART_REGS_MAP: [u32; 4] = [0x007f_ff6d, 0x0000_0010, 0, 0]; + const UART_NODE_COUNT: usize = 3; + + // SysTimer (base 0x6000_A000). Register offsets and bitfield masks from + // ESP-IDF v5.4 `systimer_reg.h`; node sequence from + // `systimer_regs_retention[]`. + const ST_BASE: u32 = 0x6000_A000; + const ST_CONF: u32 = ST_BASE; // +0x00 + const ST_UNIT0_OP: u32 = ST_BASE + 0x04; + const ST_UNIT1_OP: u32 = ST_BASE + 0x08; + const ST_UNIT0_LOAD_HI: u32 = ST_BASE + 0x0C; + const ST_UNIT1_LOAD_HI: u32 = ST_BASE + 0x14; + const ST_TARGET0_HI: u32 = ST_BASE + 0x1C; + const ST_TARGET0_CONF: u32 = ST_BASE + 0x34; + const ST_TARGET1_CONF: u32 = ST_BASE + 0x38; + const ST_TARGET2_CONF: u32 = ST_BASE + 0x3C; + const ST_UNIT0_VALUE_HI: u32 = ST_BASE + 0x40; + const ST_UNIT1_VALUE_HI: u32 = ST_BASE + 0x48; + const ST_COMP0_LOAD: u32 = ST_BASE + 0x50; + const ST_COMP1_LOAD: u32 = ST_BASE + 0x54; + const ST_COMP2_LOAD: u32 = ST_BASE + 0x58; + const ST_UNIT0_LOAD: u32 = ST_BASE + 0x5C; + const ST_UNIT1_LOAD: u32 = ST_BASE + 0x60; + const ST_INT_ENA: u32 = ST_BASE + 0x64; + const ST_UNIT_UPDATE: u32 = 1 << 30; + const ST_UNIT_VALUE_VALID: u32 = 1 << 29; + const ST_UNIT_LOAD: u32 = 1 << 0; + const ST_COMP_LOAD: u32 = 1 << 0; + const ST_TARGET_PERIOD_MODE: u32 = 1 << 30; + /// TARGET0_HI ..= TARGET2_CONF, i.e. all three targets' hi/lo/conf. + const ST_TARGETS_LEN: u32 = 9; + + const SYSTIMER_NODE_COUNT: usize = 19; + /// SysTimer CONTINUOUS-node words: unit0/1 value (2+2), targets (9), + /// conf (1) and int_ena (1). + const SYSTIMER_CONT_WORDS: usize = 2 + 2 + ST_TARGETS_LEN as usize + 1 + 1; + + /// One node per continuous region, the TEE-critical WRITE node, the console + /// UART sequence, and the SysTimer sequence. + const NODE_COUNT: usize = CONT_REGIONS.len() + 1 + UART_NODE_COUNT + SYSTIMER_NODE_COUNT; + const BUF_WORDS: usize = + total_words() + UART_RETENTION_REGS_CNT as usize + SYSTIMER_CONT_WORDS; + + static mut NODES: [RegdmaLink; NODE_COUNT] = [RegdmaLink::EMPTY; NODE_COUNT]; + static mut BUF: [u32; BUF_WORDS] = [0; BUF_WORDS]; + + // Every region count must fit the 10-bit `length` field of a regDMA node. + const _: () = { + let mut i = 0; + while i < CONT_REGIONS.len() { + assert!(CONT_REGIONS[i].count <= HEAD_LENGTH_MASK); + i += 1; + } + }; + + /// (Re)build the SYS_PERIPH retention linked list into static storage and + /// return the node slice ready to be chained/triggered. + /// + /// Rebuilding on each call keeps the nodes' `next`/buffer pointers + /// self-consistent and is cheap (a few dozen writes). + pub(super) fn build_link() -> &'static mut [RegdmaLink] { + // SAFETY: retention is driven from a single context around sleep; there + // is no concurrent access to these statics. + let nodes = unsafe { &mut *core::ptr::addr_of_mut!(NODES) }; + let buf_base = core::ptr::addr_of_mut!(BUF) as *mut u32; + + let mut node = 0; + let mut word = 0; + + // PRI_0: system clock (PCR). + for region in &CONT_REGIONS[..TEE_APM_START] { + let mem = unsafe { buf_base.add(word) } as u32; + nodes[node] = RegdmaLink::continuous(region.base, mem, region.count); + word += region.count as usize; + node += 1; + } + + // PRI_2: TEE-critical WRITE node (restore-only: skip on backup). Clears + // TEE_M4_MODE_CTRL so the following TEE/APM restore can write freely. + nodes[node] = RegdmaLink::write(TEE_M4_MODE_CTRL_REG, 0, 0xFFFF_FFFF, true, false); + node += 1; + + // PRI_4/5: TEE/APM, interrupt matrix, HP system. + for region in &CONT_REGIONS[TEE_APM_START..IOMUX_START] { + let mem = unsafe { buf_base.add(word) } as u32; + nodes[node] = RegdmaLink::continuous(region.base, mem, region.count); + word += region.count as usize; + node += 1; + } + + // PRI_5: console UART0. ADDR_MAP restores the config registers, then a + // restore-only WRITE+WAIT pulses UART_REG_UPDATE to latch the shadow + // registers. The WRITE/WAIT skip the backup pass (they only matter on + // restore), so the backup just reads the selected registers. + let mem = unsafe { buf_base.add(word) } as u32; + nodes[node] = RegdmaLink::addr_map( + UART_INT_ENA_REG, + mem, + UART_RETENTION_REGS_CNT, + UART_REGS_MAP, + ); + word += UART_RETENTION_REGS_CNT as usize; + node += 1; + nodes[node] = RegdmaLink::write( + UART_REG_UPDATE_REG, + UART_REG_UPDATE, + UART_REG_UPDATE, + true, + false, + ); + node += 1; + nodes[node] = RegdmaLink::wait(UART_REG_UPDATE_REG, 0, UART_REG_UPDATE, true, false); + node += 1; + + // PRI_6: IO MUX, GPIO matrix, SPI mem. + for region in &CONT_REGIONS[IOMUX_START..] { + let mem = unsafe { buf_base.add(word) } as u32; + nodes[node] = RegdmaLink::continuous(region.base, mem, region.count); + word += region.count as usize; + node += 1; + } + + // PRI_6: SysTimer. Backup latches each unit's counter (UPDATE + wait for + // VALUE_VALID) and reads the value; restore writes the value into the + // LOAD registers and triggers a load. The counter value is read from the + // VALUE_HI/LO registers but restored into the LOAD_HI/LO registers, so + // those CONTINUOUS nodes use split backup/restore addresses. + // + // `alloc` reserves `len` words of buffer and returns their address; it + // captures only the (copyable) buffer base, leaving `nodes` free to + // index directly. + let alloc = |len: u32, word: &mut usize| -> u32 { + let mem = unsafe { buf_base.add(*word) } as u32; + *word += len as usize; + mem + }; + + // Unit 0: latch + read value, restore into load. + nodes[node] = RegdmaLink::write(ST_UNIT0_OP, ST_UNIT_UPDATE, ST_UNIT_UPDATE, false, true); + node += 1; + nodes[node] = RegdmaLink::wait( + ST_UNIT0_OP, + ST_UNIT_VALUE_VALID, + ST_UNIT_VALUE_VALID, + false, + true, + ); + node += 1; + let mem = alloc(2, &mut word); + nodes[node] = RegdmaLink::continuous_split(ST_UNIT0_VALUE_HI, ST_UNIT0_LOAD_HI, mem, 2); + node += 1; + nodes[node] = RegdmaLink::write(ST_UNIT0_LOAD, ST_UNIT_LOAD, ST_UNIT_LOAD, true, false); + node += 1; + + // Unit 1. + nodes[node] = RegdmaLink::write(ST_UNIT1_OP, ST_UNIT_UPDATE, ST_UNIT_UPDATE, false, true); + node += 1; + nodes[node] = RegdmaLink::wait( + ST_UNIT1_OP, + ST_UNIT_VALUE_VALID, + ST_UNIT_VALUE_VALID, + false, + true, + ); + node += 1; + let mem = alloc(2, &mut word); + nodes[node] = RegdmaLink::continuous_split(ST_UNIT1_VALUE_HI, ST_UNIT1_LOAD_HI, mem, 2); + node += 1; + nodes[node] = RegdmaLink::write(ST_UNIT1_LOAD, ST_UNIT_LOAD, ST_UNIT_LOAD, true, false); + node += 1; + + // Comparator target values & periods. + let mem = alloc(ST_TARGETS_LEN, &mut word); + nodes[node] = RegdmaLink::continuous(ST_TARGET0_HI, mem, ST_TARGETS_LEN); + node += 1; + for comp in [ST_COMP0_LOAD, ST_COMP1_LOAD, ST_COMP2_LOAD] { + nodes[node] = RegdmaLink::write(comp, ST_COMP_LOAD, ST_COMP_LOAD, true, false); + node += 1; + } + // Re-arm period mode: clear then set for target0/1, clear for target2 + // (matches ESP-IDF's write sequence). + for target in [ST_TARGET0_CONF, ST_TARGET1_CONF] { + nodes[node] = RegdmaLink::write(target, 0, ST_TARGET_PERIOD_MODE, true, false); + node += 1; + nodes[node] = RegdmaLink::write( + target, + ST_TARGET_PERIOD_MODE, + ST_TARGET_PERIOD_MODE, + true, + false, + ); + node += 1; + } + nodes[node] = RegdmaLink::write(ST_TARGET2_CONF, 0, ST_TARGET_PERIOD_MODE, true, false); + node += 1; + + // Work-enable and interrupt-enable state. + let mem = alloc(1, &mut word); + nodes[node] = RegdmaLink::continuous(ST_CONF, mem, 1); + node += 1; + let mem = alloc(1, &mut word); + nodes[node] = RegdmaLink::continuous(ST_INT_ENA, mem, 1); + node += 1; + + &mut nodes[..node] + } +} diff --git a/esp-hal/src/rtc_cntl/sleep/esp32c6.rs b/esp-hal/src/rtc_cntl/sleep/esp32c6.rs index c18f54a036f..c386a1c1361 100644 --- a/esp-hal/src/rtc_cntl/sleep/esp32c6.rs +++ b/esp-hal/src/rtc_cntl/sleep/esp32c6.rs @@ -570,6 +570,12 @@ pub struct RtcSleepConfig { pub deep: bool, /// Power Down flags pub pd_flags: PowerDownFlags, + /// Power down the CPU power domain during light sleep. See + /// [`Self::with_cpu_power_down`]. + cpu_power_down: bool, + /// Power down the digital TOP power domain during light sleep. See + /// [`Self::with_top_power_down`]. + top_power_down: bool, } impl Default for RtcSleepConfig { @@ -580,10 +586,62 @@ impl Default for RtcSleepConfig { Self { deep: false, pd_flags: PowerDownFlags(0), + cpu_power_down: false, + top_power_down: false, } } } +impl RtcSleepConfig { + /// Power down the CPU power domain during light sleep. + /// + /// When enabled, the CPU's register state is saved before sleeping and + /// restored on wakeup entirely in software (the regDMA/PAU engine is not + /// involved), allowing the CPU domain to be powered off for additional + /// savings. This has no effect on deep sleep, where the CPU is always + /// powered down. + /// + /// Use [`cpu_retention::cpu_power_down_wake_count`] to confirm the CPU + /// domain actually lost power across a sleep. + /// + /// [`cpu_retention::cpu_power_down_wake_count`]: crate::rtc_cntl::cpu_retention::cpu_power_down_wake_count + #[instability::unstable] + #[must_use] + pub fn with_cpu_power_down(mut self, enable: bool) -> Self { + self.cpu_power_down = enable; + self + } + + /// Returns whether the CPU power domain is powered down during light sleep. + #[instability::unstable] + pub fn cpu_power_down(&self) -> bool { + self.cpu_power_down + } + + /// Power down the digital `TOP` power domain during light sleep. + /// + /// The `TOP` domain holds the core system peripherals (interrupt matrix, + /// HP system, TEE/APM, IO MUX, flash SPI mem, SysTimer, and the PCR clock + /// config). When enabled, their register state is backed up to RAM by the + /// regDMA/PAU engine before sleeping and restored on wakeup, so the domain + /// can be powered off for additional savings. Execution resumes in place + /// (the CPU domain is unaffected unless [`Self::with_cpu_power_down`] is also + /// set). This has no effect on deep sleep. + #[instability::unstable] + #[must_use] + pub fn with_top_power_down(mut self, enable: bool) -> Self { + self.top_power_down = enable; + self + } + + /// Returns whether the digital TOP power domain is powered down during light + /// sleep. + #[instability::unstable] + pub fn top_power_down(&self) -> bool { + self.top_power_down + } +} + bitfield::bitfield! { #[derive(Clone, Copy)] /// Power domains to be powered down during sleep @@ -733,6 +791,25 @@ impl RtcSleepConfig { self.pd_flags.set_pd_xtal(true); self.pd_flags.set_pd_rc_fast(true); self.pd_flags.set_pd_xtal32k(!lp_slow_uses_xtal32k); + + // Optionally power down the CPU domain. Its register state is saved + // and restored in software around the sleep (see `cpu_retention`). + if self.cpu_power_down { + self.pd_flags.set_pd_cpu(true); + } + + // Optionally power down the digital TOP domain. Its peripheral + // register state is backed up/restored by regDMA around the sleep + // (see `retention`). On C6 the CPU cannot survive a TOP power-down + // (its execution context is torn down and the ROM restarts on + // wakeup), so TOP power-down implies CPU power-down: IDF routes + // both through the software CPU-retention wake-stub path (see + // esp-idf sleep_modes.c `esp_sleep_cpu_retention` gated on + // PMU_SLEEP_PD_CPU | PMU_SLEEP_PD_TOP). + if self.top_power_down { + self.pd_flags.set_pd_top(true); + self.pd_flags.set_pd_cpu(true); + } } } @@ -822,18 +899,49 @@ impl RtcSleepConfig { // Start entry into sleep mode - // pmu_ll_hp_set_sleep_enable - PMU::regs() - .slp_wakeup_cntl0() - .write(|w| w.sleep_req().bit(true)); - - // In pd_cpu lightsleep and deepsleep mode, we never get here - loop { - let int_raw = PMU::regs().int_raw().read(); - if int_raw.soc_wakeup().bit_is_set() || int_raw.soc_sleep_reject().bit_is_set() { - break; + // Arm regDMA retention of the TOP-domain peripherals so the PMU backs + // them up on sleep entry and restores them on wakeup. Must happen after + // the power config write above (which resets the PMU backup-enable bits) + // and before the sleep request. + if !self.deep && self.pd_flags.pd_top() { + crate::rtc_cntl::retention::enable_top_retention(); + } + + if !self.deep && self.pd_flags.pd_cpu() { + // CPU power-down light sleep: save the CPU register state, trigger + // sleep and (on the same call) resume here after wakeup with the + // state restored. The sleep trigger + wait loop happen inside. + unsafe { + crate::rtc_cntl::cpu_retention::sleep_with_cpu_retention(); + } + } else { + // pmu_ll_hp_set_sleep_enable + PMU::regs() + .slp_wakeup_cntl0() + .write(|w| w.sleep_req().bit(true)); + + // In pd_cpu lightsleep and deepsleep mode, we never get here + loop { + let int_raw = PMU::regs().int_raw().read(); + if int_raw.soc_wakeup().bit_is_set() || int_raw.soc_sleep_reject().bit_is_set() { + break; + } } } + + // After a TOP-domain power-down light sleep, TIMG0's flashboot watchdog + // comes back armed: TIMG0 sits in the TOP domain, is reset when TOP + // powers up, and is deliberately not part of the regDMA retention list. + // IDF disables it in software on wakeup (misc_modules_wake_prepare(), + // sleep_modes.c). Do the same here or it will reset the chip shortly + // after we resume. + if !self.deep && self.pd_flags.pd_top() { + let tg0 = crate::peripherals::TIMG0::regs(); + tg0.wdtwprotect().write(|w| unsafe { w.bits(0x50D8_3AA1) }); + tg0.wdtconfig0() + .modify(|_, w| w.wdt_flashboot_mod_en().bit(false)); + tg0.wdtwprotect().write(|w| unsafe { w.bits(0) }); + } } /// Cleans up after sleep diff --git a/qa-test/src/bin/sleep_timer_powerdown.rs b/qa-test/src/bin/sleep_timer_powerdown.rs new file mode 100644 index 00000000000..d0c6f8a5249 --- /dev/null +++ b/qa-test/src/bin/sleep_timer_powerdown.rs @@ -0,0 +1,151 @@ +//! Timer-woken light sleep with the CPU/TOP power domains powered down, with a +//! built-in software proof that power was actually removed. +//! +//! The example walks three sleep policies for the same timer wakeup: +//! +//! - **clock-gated** ([`RtcSleepConfig::default`]): a plain light sleep. The CPU is only +//! clock-gated - execution resumes in place and no register state is lost. +//! - **cpu-powerdown** ([`with_cpu_power_down`]): the CPU domain is powered off during sleep. Its +//! state is saved/restored in software (the ROM wake stub, see `cpu_retention`), so execution +//! still resumes in place. +//! - **top-powerdown** ([`with_top_power_down`]): the whole digital `TOP` domain is powered off. +//! The core system peripherals (interrupt matrix, HP system, TEE/APM, IO MUX, flash SPI mem, +//! SysTimer, PCR clocks, console UART) lose their state, so the regDMA/PAU engine backs them up +//! to RAM before sleep and the PMU restores them on wakeup. On the C6 this also powers down the +//! CPU domain. +//! +//! ## Software proof (no instruments) +//! +//! [`cpu_power_down_wake_count`] is incremented from *inside* the ROM wake-stub +//! restore path, so it advances **only** when the CPU domain genuinely lost +//! power. For each round the example prints the wall-clock time actually spent +//! asleep (measured with the always-on RTC, which no sleep can stop) and that +//! counter: +//! +//! ```text +//! clock-gated #1: slept ~1000 ms (RTC), CPU power-downs = 0 +//! cpu-powerdown #1: slept ~1000 ms (RTC), CPU power-downs = 1 +//! top-powerdown #1: slept ~1000 ms (RTC), CPU power-downs = 4 +//! ``` +//! +//! The counter stays `0` for the clock-gated rounds and increments for every +//! power-down round - that is the definitive proof the CPU domain lost power and +//! resumed through the wake stub. Every mode sleeps for the full duration (the +//! RTC confirms the chip idled, it did not busy-wait), and there is no ROM +//! reboot banner between rounds, which shows peripheral state survived. +//! +//! The *system timer* ([`esp_hal::time::Instant`]) keeps counting through light +//! sleep on the C6 (it stays clocked for timekeeping), so it is deliberately not +//! used as the proof here. +//! +//! ## Measuring current (e.g. Nordic PPK2) +//! +//! GPIO5 is driven high while awake and low while asleep, so it brackets each +//! sleep window on a PPK2/logic-analyzer digital channel. Power the module's +//! 3V3 rail from the PPK2 in source-meter mode (USB unplugged), wire GPIO5 to a +//! logic input, and average the current over a sleep plateau. Because the three +//! modes run back-to-back you get their sleep floors in one capture: each +//! deeper power-down should show a lower floor, with small current shoulders at +//! the window edges from the regDMA / CPU-context save & restore. + +//% CHIP_FILTER: esp32c6 + +#![no_std] +#![no_main] + +use esp_backtrace as _; +use esp_hal::{ + delay::Delay, + gpio::{Level, Output, OutputConfig}, + main, + rtc_cntl::{ + Rtc, + cpu_retention::cpu_power_down_wake_count, + sleep::{RtcSleepConfig, TimerWakeupSource}, + }, + time::Duration, +}; +use esp_println::println; + +esp_bootloader_esp_idf::esp_app_desc!(); + +/// Sleep duration per round, in milliseconds. Long enough to give a current +/// meter a wide, flat sleep plateau to average over. +const EVENT_MS: u64 = 1000; +/// Awake window between sleeps, in milliseconds, so the sleep plateaus are +/// clearly separated on a current/logic trace. +const AWAKE_MS: u32 = 200; +/// Rounds per mode. +const ROUNDS: u32 = 3; + +/// Sleep once for `EVENT_MS`, then report the wall-clock time spent asleep (from +/// the always-on RTC) and the CPU power-down wake counter. `marker` is driven +/// low for the sleep window (high while awake) so a PPK2/logic channel can +/// bracket each sleep. +fn sleep_round( + rtc: &mut Rtc<'_>, + marker: &mut Output<'_>, + delay: &Delay, + config: &RtcSleepConfig, + label: &str, + round: u32, +) { + let timer = TimerWakeupSource::new(Duration::from_millis(EVENT_MS)); + + let rtc_before = rtc.time_since_power_up().as_micros(); + marker.set_low(); + rtc.sleep(config, &[&timer]); + marker.set_high(); + let slept_ms = (rtc.time_since_power_up().as_micros() - rtc_before) / 1000; + + println!( + "{} #{}: slept ~{} ms (RTC), CPU power-downs = {}", + label, + round, + slept_ms, + cpu_power_down_wake_count() + ); + + delay.delay_millis(AWAKE_MS); +} + +#[main] +fn main() -> ! { + let peripherals = esp_hal::init(esp_hal::Config::default()); + let mut rtc = Rtc::new(peripherals.LPWR); + let delay = Delay::new(); + + // Awake = high, asleep = low. The IO domain stays powered through light + // sleep, so the pin holds its level and a meter/scope sees a clean window. + let mut marker = Output::new(peripherals.GPIO5, Level::High, OutputConfig::default()); + + // Same timer wakeup, increasingly aggressive power policies. + let modes: [(&str, RtcSleepConfig); 3] = [ + ("clock-gated ", RtcSleepConfig::default()), + ( + "cpu-powerdown", + RtcSleepConfig::default().with_cpu_power_down(true), + ), + ( + "top-powerdown", + RtcSleepConfig::default().with_top_power_down(true), + ), + ]; + + println!("up and running!"); + + for (label, config) in &modes { + for round in 1..=ROUNDS { + sleep_round(&mut rtc, &mut marker, &delay, config, label, round); + } + } + + // Keep going in the deepest mode so the counter can be watched climbing and + // a meter has a steady stream of identical sleep windows to average. + let top = RtcSleepConfig::default().with_top_power_down(true); + let mut round = ROUNDS; + loop { + round += 1; + sleep_round(&mut rtc, &mut marker, &delay, &top, "top-powerdown", round); + } +} From d53c1e0d3259055cc26b5814210eae83d9e719a9 Mon Sep 17 00:00:00 2001 From: Juraj Sadel Date: Thu, 2 Jul 2026 16:00:02 +0200 Subject: [PATCH 2/8] rework rtc_cntl: rework retention API per review feedback --- esp-hal/src/i2c/master/mod.rs | 27 + esp-hal/src/rtc_cntl/cpu_retention.rs | 262 ++++---- esp-hal/src/rtc_cntl/mod.rs | 11 +- esp-hal/src/rtc_cntl/power_domain.rs | 58 ++ esp-hal/src/rtc_cntl/retention.rs | 778 ++++++++++++++++------- esp-hal/src/rtc_cntl/sleep/esp32c6.rs | 137 ++-- esp-hal/src/spi/master/low_level/mod.rs | 8 + esp-hal/src/spi/master/mod.rs | 14 + esp-hal/src/uart/mod.rs | 30 + qa-test/src/bin/sleep_timer_powerdown.rs | 301 ++++++--- 10 files changed, 1135 insertions(+), 491 deletions(-) create mode 100644 esp-hal/src/rtc_cntl/power_domain.rs diff --git a/esp-hal/src/i2c/master/mod.rs b/esp-hal/src/i2c/master/mod.rs index 7dac298bcd9..4a1d87e4575 100644 --- a/esp-hal/src/i2c/master/mod.rs +++ b/esp-hal/src/i2c/master/mod.rs @@ -153,6 +153,10 @@ mod low_level; pub use low_level::{AnyI2c, Instance}; use low_level::{Driver, I2cClockGuard}; +#[cfg(esp32c6)] +#[instability::unstable] +pub use crate::rtc_cntl::retention::I2cRetentionMemory; + const I2C_FIFO_SIZE: usize = property!("i2c_master.fifo_size"); // Chunk writes/reads by this size const I2C_CHUNK_SIZE: usize = I2C_FIFO_SIZE - 1; @@ -680,6 +684,12 @@ pub struct I2c<'d, Dm: DriverMode> { phantom: PhantomData, guard: PeripheralGuard, config: DriverConfig, + // Active keeps `TOP` powered; `I2c::with_retention_memory` swaps to retained. + #[cfg(esp32c6)] + power: crate::rtc_cntl::retention::PowerManagement< + 'd, + crate::rtc_cntl::retention::I2cRetentionMemory, + >, } #[derive(Debug)] @@ -736,6 +746,8 @@ impl<'d> I2c<'d, Blocking> { sda_pin, scl_pin, }, + #[cfg(esp32c6)] + power: crate::rtc_cntl::retention::PowerManagement::new(), }; // Make sure inputs are well-defined. @@ -759,6 +771,8 @@ impl<'d> I2c<'d, Blocking> { phantom: PhantomData, guard: self.guard, config: self.config, + #[cfg(esp32c6)] + power: self.power, } } @@ -851,6 +865,8 @@ impl<'d> I2c<'d, Async> { phantom: PhantomData, guard: self.guard, config: self.config, + #[cfg(esp32c6)] + power: self.power, } } @@ -1038,6 +1054,17 @@ where self.driver().reset_fsm(*error == Error::Timeout) } + /// Retain this I2C's config registers in `mem` across a `TOP` power-down in + /// light sleep. While active the driver keeps `TOP` powered; this drops that + /// lock and lets regDMA save/restore the config so `TOP` can power down. + #[cfg(esp32c6)] + #[instability::unstable] + pub fn with_retention_memory(mut self, mem: &'d mut I2cRetentionMemory) -> Self { + let base = self.i2c.info().regs() as *const RegisterBlock as usize as u32; + self.power.retain(mem, base); + self + } + #[procmacros::doc_replace] /// Connect a pin to the I2C SDA signal. /// diff --git a/esp-hal/src/rtc_cntl/cpu_retention.rs b/esp-hal/src/rtc_cntl/cpu_retention.rs index 663667a2fbd..b61571f997b 100644 --- a/esp-hal/src/rtc_cntl/cpu_retention.rs +++ b/esp-hal/src/rtc_cntl/cpu_retention.rs @@ -1,72 +1,50 @@ -//! # CPU power-down retention during light sleep (ESP32-C6) +//! CPU power-down retention during light sleep (ESP32-C6). //! -//! ## Overview +//! During light sleep the C6 can power down the CPU domain (`pd_cpu`) while the +//! rest of the digital system stays powered, losing all CPU state. The register +//! file and CSRs aren't reachable by regDMA, so (like ESP-IDF's +//! `esp_sleep_cpu_retention()`) they are saved/restored in software. The ~1 KiB +//! backing RAM ([`CpuRetentionMemory`]) is caller-owned and opt-in via +//! [`RtcSleepConfig::with_cpu_power_down`]; without it the CPU is only +//! clock-gated. //! -//! During light sleep the ESP32-C6 can additionally power **down the CPU power -//! domain** (`pd_cpu`) while the rest of the digital system (the `TOP` domain: -//! RAM, peripherals, ...) stays powered. Powering the CPU down loses all of its -//! state, so before sleeping we save everything required to resume execution and -//! restore it on wakeup. +//! [`RtcSleepConfig::with_cpu_power_down`]: crate::rtc_cntl::sleep::RtcSleepConfig::with_cpu_power_down //! -//! Unlike peripheral (`TOP`-domain) retention, CPU retention does **not** use the -//! regDMA/PAU engine. The CPU register file and CSRs are not reachable by regDMA, -//! so ESP-IDF saves/restores them in **software**, which is exactly what this -//! module does. It mirrors `esp_sleep_cpu_retention()` in -//! `components/esp_hw_support/lowpower/port/esp32c6/sleep_cpu.c`. +//! Save/restore is in three parts, matching ESP-IDF: //! -//! The save/restore is split into three parts, matching ESP-IDF: -//! -//! 1. **Critical registers** - the general-purpose registers and the handful of -//! machine CSRs needed to resume the interrupted control flow (`mepc`, -//! `mstatus`, `mtvec`, ...). Saved and restored in assembly -//! (`rv_core_critical_regs_save` / `rv_core_critical_regs_restore`), using a -//! `setjmp`/`longjmp`-style trick: the save routine records the return -//! context and, on wakeup, the ROM jumps to the restore routine which returns -//! *as if the save routine had just returned*. -//! 2. **Non-critical CSRs** - the rest of the architectural CSR state (PMP/PMA, -//! trigger module, performance counters, ...). Saved/restored in Rust via +//! 1. **Critical registers** (GP registers + `mepc`/`mstatus`/`mtvec`/...): +//! saved/restored in assembly via a `setjmp`/`longjmp`-style trick - on +//! wakeup the ROM jumps to the restore routine, which returns as if save had. +//! 2. **Non-critical CSRs** (PMP/PMA, trigger module, perf counters, ...): via //! `csrr`/`csrw`. -//! 3. **CPU-domain device registers** - memory-mapped registers that live in the -//! CPU power domain (interrupt matrix priority `INTPRI`, the `PLIC`/`CLINT` -//! interrupt controllers and the L1 cache control). Saved/restored with plain -//! loads/stores. -//! -//! ## Wakeup path +//! 3. **CPU-domain device registers** (`INTPRI`, `PLIC`/`CLINT`, L1 cache). //! -//! The whole save -> sleep -> restore path runs from **internal RAM** (`.rwtext`, -//! i.e. IRAM). This is mandatory: when the CPU is powered back up the ROM jumps -//! directly to the wake-stub address we program into `LP_AON_STORE8` -//! (`RTC_SLEEP_WAKE_STUB_ADDR_REG`), and at that point the flash cache state has -//! been lost. Only after the cache configuration is restored may we touch flash -//! again, so every function on this path is annotated `#[ram]` and must avoid -//! calling into flash-resident code. +//! The whole path runs from IRAM (`.rwtext`): the ROM jumps to the wake stub in +//! `LP_AON_STORE8` with the flash cache lost, so every function here is `#[ram]` +//! and must not call flash-resident code until the cache config is restored. //! -//! References (ESP-IDF `v5.4`, commit -//! `8e27ea72c6688b79348b123ff40d556cfe16c8c3`, ESP32-C6): -//! - [`sleep_cpu.c`](https://github.com/espressif/esp-idf/blob/8e27ea72c6688b79348b123ff40d556cfe16c8c3/components/esp_hw_support/lowpower/port/esp32c6/sleep_cpu.c) -//! - [`sleep_cpu_asm.S`](https://github.com/espressif/esp-idf/blob/8e27ea72c6688b79348b123ff40d556cfe16c8c3/components/esp_hw_support/lowpower/port/esp32c6/sleep_cpu_asm.S) -//! - [`rvsleep-frames.h`](https://github.com/espressif/esp-idf/blob/8e27ea72c6688b79348b123ff40d556cfe16c8c3/components/esp_hw_support/lowpower/port/esp32c6/include/rvsleep-frames.h) +//! References (ESP-IDF `v5.4`, ESP32-C6): `esp_hw_support/.../esp32c6/` +//! `sleep_cpu.c`, `sleep_cpu_asm.S`, `include/rvsleep-frames.h`. -use core::{ - ptr::addr_of_mut, - sync::atomic::{AtomicU32, Ordering}, -}; +use core::sync::atomic::{AtomicU32, Ordering}; use procmacros::ram; use crate::peripherals::{LP_AON, PMU}; -/// Number of times execution resumed through the ROM wake stub, i.e. how many -/// times the CPU power domain was actually powered down and restored. A sleep -/// that was rejected or where the CPU stayed powered does *not* increment this. +/// Caller-owned backing store for TOP-domain system-peripheral retention. +/// +/// Re-exported here because it is the second buffer required by +/// [`RtcSleepConfig::with_top_power_down`](crate::rtc_cntl::sleep::RtcSleepConfig::with_top_power_down). +#[instability::unstable] +pub use crate::rtc_cntl::retention::SystemRetentionMemory; + +/// Bumped from the ROM wake stub, i.e. only when the CPU domain actually lost +/// and regained power (not on a rejected or clock-gated sleep). static CPU_POWERDOWN_WAKES: AtomicU32 = AtomicU32::new(0); -/// Returns how many times the CPU power domain has actually been powered down -/// and successfully restored via the ROM wake stub. -/// -/// This is primarily a diagnostic: if it increases across light sleeps then the -/// CPU genuinely lost power (rather than the request being rejected or the CPU -/// merely clock-gated). +/// How many times the CPU power domain was actually powered down and restored. +/// A diagnostic: if it rises across light sleeps, the CPU genuinely lost power. #[instability::unstable] pub fn cpu_power_down_wake_count() -> u32 { CPU_POWERDOWN_WAKES.load(Ordering::Relaxed) @@ -76,10 +54,8 @@ pub fn cpu_power_down_wake_count() -> u32 { // Critical register frame (RvCoreCriticalSleepFrame) // --------------------------------------------------------------------------- -// The critical frame is a raw word buffer, not a typed struct: the assembly -// below is its only accessor and addresses every slot by byte offset -// (`RV_SLP_CTX_*`), so Rust just needs a correctly-sized, 4-byte-aligned buffer. -// The layout, word for word, matches ESP-IDF's `rvsleep-frames.h`: +// A raw word buffer addressed by byte offset (`RV_SLP_CTX_*`) from the assembly +// below; layout matches ESP-IDF's `rvsleep-frames.h`: // // 0: mepc 1: ra 2: sp 3: gp 4: tp // 5: t0 6: t1 7: t2 8: s0 9: s1 @@ -87,30 +63,24 @@ pub fn cpu_power_down_wake_count() -> u32 { // 32: mstatus 33: mtvec 34: mcause 35: mtval 36: mie 37: mip 38: pmufunc const CRITICAL_FRAME_WORDS: usize = 39; -/// Word index of the `pmufunc` slot (byte offset `RV_SLP_CTX_PMUFUNC` = 152). -/// `pmufunc & 0x3` encodes the phase: `1` = going to sleep, `3` = resumed via -/// the wake stub. +/// `pmufunc` slot. `pmufunc & 0x3`: `1` = going to sleep, `3` = resumed via the +/// wake stub. const PMUFUNC_WORD: usize = 38; -/// Backing store for the critical frame. Lives in internal RAM (`.bss`), which -/// is retained while only the CPU domain is powered down. -static mut CRITICAL_FRAME: [u32; CRITICAL_FRAME_WORDS] = [0; CRITICAL_FRAME_WORDS]; - -/// Pointer the assembly reads to find [`CRITICAL_FRAME`]. Set before sleeping. +/// Pointer the assembly reads to find the critical frame. Set before every sleep. static mut RV_CORE_CRITICAL_REGS_FRAME: *mut u32 = core::ptr::null_mut(); unsafe extern "C" { - /// Save the CPU critical registers into `RV_CORE_CRITICAL_REGS_FRAME` and - /// mark the frame as "going to sleep". Returns the frame pointer. + /// Save the CPU critical registers into `RV_CORE_CRITICAL_REGS_FRAME`, mark + /// the frame "going to sleep", and return the frame pointer. fn rv_core_critical_regs_save() -> *mut u32; - /// Restore the CPU critical registers. Used as the ROM wake stub: on wakeup - /// it returns control as if [`rv_core_critical_regs_save`] had just returned. + /// Restore the CPU critical registers. Used as the ROM wake stub: returns as + /// if [`rv_core_critical_regs_save`] had just returned. fn rv_core_critical_regs_restore() -> *mut u32; } -// Ported from ESP-IDF's `rv_core_critical_regs_save` / `..._restore` in -// `sleep_cpu_asm.S`: -// https://github.com/espressif/esp-idf/blob/8e27ea72c6688b79348b123ff40d556cfe16c8c3/components/esp_hw_support/lowpower/port/esp32c6/sleep_cpu_asm.S +// Ported from ESP-IDF's `rv_core_critical_regs_save`/`..._restore` in +// `sleep_cpu_asm.S`. core::arch::global_asm!( r#" .set RV_SLP_CTX_MEPC, 0 @@ -315,23 +285,18 @@ unsafe fn write_csr(value: u32) { } } -/// Defines the set of non-critical CSRs to retain from a single canonical list, -/// generating the backing store plus the save and restore routines so the order -/// and slot count can never drift between them. -/// -/// The list and its order mirror `rv_core_noncritical_regs_save()` / -/// `..._restore()` in ESP-IDF's [`sleep_cpu.c`](https://github.com/espressif/esp-idf/blob/8e27ea72c6688b79348b123ff40d556cfe16c8c3/components/esp_hw_support/lowpower/port/esp32c6/sleep_cpu.c#L238-L401). -/// The `$name` tokens are documentation only; the CSR is addressed by number so -/// that the custom Espressif CSRs (`pmaaddr*`/`pmacfg*`, performance counters, -/// user GPIO) work without assembler support. +/// Define the non-critical CSRs to retain from one canonical list, generating +/// the slot count and the save/restore routines so they can't drift. Order +/// mirrors `rv_core_noncritical_regs_{save,restore}()` in ESP-IDF `sleep_cpu.c`. +/// `$name` is documentation only; the CSR is addressed by number so the custom +/// Espressif CSRs work without assembler support. macro_rules! noncritical_csrs { ($($name:ident = $csr:literal),+ $(,)?) => { - /// Backing store for the non-critical CSR values, one `u32` slot each. - static mut NONCRITICAL_FRAME: [u32; [$($csr),+].len()] = [0; [$($csr),+].len()]; + /// Non-critical CSR slot count; sizes the `noncritical` field. + const NONCRITICAL_WORDS: usize = [$($csr),+].len(); #[ram] - fn save_noncritical() { - let buf = addr_of_mut!(NONCRITICAL_FRAME) as *mut u32; + fn save_noncritical(buf: *mut u32) { let mut i = 0usize; $( unsafe { buf.add(i).write(read_csr::<$csr>()); } @@ -341,8 +306,7 @@ macro_rules! noncritical_csrs { } #[ram] - fn restore_noncritical() { - let buf = addr_of_mut!(NONCRITICAL_FRAME) as *const u32; + fn restore_noncritical(buf: *const u32) { let mut i = 0usize; $( unsafe { write_csr::<$csr>(buf.add(i).read()); } @@ -400,8 +364,7 @@ struct Region { words: usize, } -/// Total number of 32-bit words covered by a set of [`Region`]s. Used to size -/// the backing stores so they always match the regions they hold. +/// Total 32-bit words covered by a set of [`Region`]s, to size their store. const fn total_words(regions: &[Region]) -> usize { let mut words = 0; let mut i = 0; @@ -448,11 +411,6 @@ const CLINT_REGIONS: [Region; 2] = [ Region { start: 0x2000_1C00, words: 6 }, ]; -static mut INTPRI_FRAME: [u32; total_words(&INTPRI_REGIONS)] = [0; total_words(&INTPRI_REGIONS)]; -static mut CACHE_FRAME: [u32; total_words(&CACHE_REGIONS)] = [0; total_words(&CACHE_REGIONS)]; -static mut PLIC_FRAME: [u32; total_words(&PLIC_REGIONS)] = [0; total_words(&PLIC_REGIONS)]; -static mut CLINT_FRAME: [u32; total_words(&CLINT_REGIONS)] = [0; total_words(&CLINT_REGIONS)]; - #[ram] fn save_device_regs(regions: &[Region], buf: *mut u32) { let mut out = buf; @@ -483,6 +441,55 @@ fn restore_device_regs(regions: &[Region], buf: *const u32) { } } +// --------------------------------------------------------------------------- +// Caller-owned retention storage +// --------------------------------------------------------------------------- + +/// Backing storage (~1 KiB) for CPU power-down register retention. +/// +/// Caller-owned, opted into via [`RtcSleepConfig::with_cpu_power_down`] (or +/// [`RtcSleepConfig::with_top_power_down`], which also powers the CPU down). +/// +/// [`RtcSleepConfig::with_cpu_power_down`]: crate::rtc_cntl::sleep::RtcSleepConfig::with_cpu_power_down +/// [`RtcSleepConfig::with_top_power_down`]: crate::rtc_cntl::sleep::RtcSleepConfig::with_top_power_down +#[instability::unstable] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(C, align(4))] +pub struct CpuRetentionMemory { + /// Critical frame: GP registers + machine CSRs, addressed by the assembly. + critical: [u32; CRITICAL_FRAME_WORDS], + /// Non-critical CSRs. + noncritical: [u32; NONCRITICAL_WORDS], + /// CPU-domain device registers. + intpri: [u32; total_words(&INTPRI_REGIONS)], + cache: [u32; total_words(&CACHE_REGIONS)], + plic: [u32; total_words(&PLIC_REGIONS)], + clint: [u32; total_words(&CLINT_REGIONS)], +} + +impl CpuRetentionMemory { + /// Create a new, zeroed CPU retention buffer. + #[instability::unstable] + pub const fn new() -> Self { + Self { + critical: [0; CRITICAL_FRAME_WORDS], + noncritical: [0; NONCRITICAL_WORDS], + intpri: [0; total_words(&INTPRI_REGIONS)], + cache: [0; total_words(&CACHE_REGIONS)], + plic: [0; total_words(&PLIC_REGIONS)], + clint: [0; total_words(&CLINT_REGIONS)], + } + } +} + +#[instability::unstable] +impl Default for CpuRetentionMemory { + fn default() -> Self { + Self::new() + } +} + // --------------------------------------------------------------------------- // Entry: save -> sleep -> restore // --------------------------------------------------------------------------- @@ -506,22 +513,17 @@ unsafe fn restore_mstatus(mstatus: u32) { } /// Save the CPU critical registers, program the wake stub, request sleep and -/// spin until the PMU reports wakeup (or rejects the request). -/// -/// Mirrors ESP-IDF's `do_cpu_retention()`: on the *save* pass `pmufunc & 0x3 == -/// 1`, so we set the wake stub and trigger sleep. On wakeup the ROM jumps to the -/// restore routine which returns here with `pmufunc & 0x3 == 3`, so we simply -/// fall through. +/// spin until the PMU reports wakeup or rejection. Mirrors ESP-IDF's +/// `do_cpu_retention()`: the save pass (`pmufunc & 0x3 == 1`) triggers sleep; on +/// wakeup the ROM jumps to the restore routine, which returns here with +/// `pmufunc & 0x3 == 3`. #[ram] fn do_cpu_retention() { let frame = unsafe { rv_core_critical_regs_save() }; let pmufunc = unsafe { frame.add(PMUFUNC_WORD).read_volatile() }; if pmufunc & 0x3 == 0x1 { - // Going to sleep. - - // RTC_SLEEP_WAKE_STUB_ADDR_REG (= LP_AON_STORE8): where the ROM jumps - // to on light-sleep CPU-power-up. + // Going to sleep. LP_AON_STORE8 is the ROM wake-stub address register. LP_AON::regs() .store8() .write(|w| unsafe { w.bits(rv_core_critical_regs_restore as *const () as usize as u32) }); @@ -529,9 +531,8 @@ fn do_cpu_retention() { // pmu_ll_hp_set_sleep_enable PMU::regs().slp_wakeup_cntl0().write(|w| w.sleep_req().bit(true)); - // In the power-down case we never get past this loop: the CPU loses - // power here and resumes via the wake stub. If the sleep is rejected we - // fall out normally. + // On power-down the CPU loses power here and resumes via the wake stub; + // on a rejected sleep we fall out normally. loop { let int_raw = PMU::regs().int_raw().read(); if int_raw.soc_wakeup().bit_is_set() || int_raw.soc_sleep_reject().bit_is_set() { @@ -539,46 +540,41 @@ fn do_cpu_retention() { } } } else if pmufunc & 0x3 == 0x3 { - // We resumed here via the ROM wake stub, which only happens after the - // CPU power domain was actually powered down and restored. + // Resumed via the ROM wake stub: the CPU domain really lost power. CPU_POWERDOWN_WAKES.fetch_add(1, Ordering::Relaxed); } } -/// Perform a full CPU-power-down light sleep with software register retention. -/// -/// This is the equivalent of ESP-IDF's `esp_sleep_cpu_retention()`. The PMU sleep -/// configuration (wakeup/reject masks, power config, ...) must already have been -/// programmed by the caller; this function only adds the CPU save/restore around -/// the actual sleep trigger. +/// Perform a full CPU-power-down light sleep with software register retention +/// (ESP-IDF's `esp_sleep_cpu_retention()`). Only adds the save/restore around +/// the sleep trigger; the PMU sleep config must already be programmed. /// /// # Safety /// -/// Must be called with the PMU already configured for a `pd_cpu` light sleep and -/// with the system in a state where stopping the CPU is safe (interrupts are -/// disabled internally for the duration). +/// The PMU must already be configured for a `pd_cpu` light sleep and stopping +/// the CPU must be safe. `mem` must stay valid across the sleep. #[ram] -pub(crate) unsafe fn sleep_with_cpu_retention() { +pub(crate) unsafe fn sleep_with_cpu_retention(mem: &mut CpuRetentionMemory) { unsafe { - RV_CORE_CRITICAL_REGS_FRAME = addr_of_mut!(CRITICAL_FRAME) as *mut u32; + RV_CORE_CRITICAL_REGS_FRAME = mem.critical.as_mut_ptr(); let mstatus = save_mstatus_and_disable_int(); - save_device_regs(&PLIC_REGIONS, addr_of_mut!(PLIC_FRAME) as *mut u32); - save_device_regs(&CLINT_REGIONS, addr_of_mut!(CLINT_FRAME) as *mut u32); - save_device_regs(&INTPRI_REGIONS, addr_of_mut!(INTPRI_FRAME) as *mut u32); - save_device_regs(&CACHE_REGIONS, addr_of_mut!(CACHE_FRAME) as *mut u32); - save_noncritical(); + save_device_regs(&PLIC_REGIONS, mem.plic.as_mut_ptr()); + save_device_regs(&CLINT_REGIONS, mem.clint.as_mut_ptr()); + save_device_regs(&INTPRI_REGIONS, mem.intpri.as_mut_ptr()); + save_device_regs(&CACHE_REGIONS, mem.cache.as_mut_ptr()); + save_noncritical(mem.noncritical.as_mut_ptr()); do_cpu_retention(); - // Restored in the reverse order of saving. The cache configuration must - // come back before we return to flash-resident code. - restore_noncritical(); - restore_device_regs(&CACHE_REGIONS, addr_of_mut!(CACHE_FRAME) as *const u32); - restore_device_regs(&INTPRI_REGIONS, addr_of_mut!(INTPRI_FRAME) as *const u32); - restore_device_regs(&CLINT_REGIONS, addr_of_mut!(CLINT_FRAME) as *const u32); - restore_device_regs(&PLIC_REGIONS, addr_of_mut!(PLIC_FRAME) as *const u32); + // Restore in reverse order; the cache config must come back before we + // return to flash-resident code. + restore_noncritical(mem.noncritical.as_ptr()); + restore_device_regs(&CACHE_REGIONS, mem.cache.as_ptr()); + restore_device_regs(&INTPRI_REGIONS, mem.intpri.as_ptr()); + restore_device_regs(&CLINT_REGIONS, mem.clint.as_ptr()); + restore_device_regs(&PLIC_REGIONS, mem.plic.as_ptr()); restore_mstatus(mstatus); } diff --git a/esp-hal/src/rtc_cntl/mod.rs b/esp-hal/src/rtc_cntl/mod.rs index 80fbaa5c543..c88f7d37de9 100644 --- a/esp-hal/src/rtc_cntl/mod.rs +++ b/esp-hal/src/rtc_cntl/mod.rs @@ -124,14 +124,15 @@ use crate::{peripherals::RTC_TIMER, system::Cpu, time::Duration}; #[cfg(sleep_driver_supported)] pub mod sleep; -// regDMA/PAU-based register retention of the TOP power domain's peripherals -// during light sleep. C6-only for now. Internal: driven automatically by the -// sleep path when `pd_top` is requested. +// Power-domain locks that keep a domain powered across light sleep. +#[cfg(esp32c6)] +pub(crate) mod power_domain; + +// regDMA-based register retention of the TOP domain's peripherals. #[cfg(esp32c6)] pub(crate) mod retention; -// Software CPU-register retention for CPU power-down during light sleep. -// C6-only for now. +// Software CPU-register retention for CPU power-down. #[cfg(esp32c6)] pub mod cpu_retention; diff --git a/esp-hal/src/rtc_cntl/power_domain.rs b/esp-hal/src/rtc_cntl/power_domain.rs new file mode 100644 index 00000000000..b48f10728bc --- /dev/null +++ b/esp-hal/src/rtc_cntl/power_domain.rs @@ -0,0 +1,58 @@ +//! Power-domain locks for light sleep (ESP32-C6). +//! +//! An active, un-retained peripheral in a power-downable domain holds a +//! [`PowerDomainLock`]: unlike a [`WakeLock`](crate::rtc_cntl::WakeLock) it +//! doesn't prevent light sleep, only powering its domain down (which degrades to +//! clock-gating), so it can't lose state. Retaining the peripheral drops the +//! lock and lets regDMA save/restore its state around the power-down instead. + +use core::sync::atomic::{AtomicU32, Ordering}; + +/// A power domain that can be independently powered down during light sleep. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Domain { + /// The CPU power domain (`pd_cpu`). + Cpu = 0, + /// The digital `TOP` power domain (`pd_top`). + Top = 1, +} + +const DOMAIN_COUNT: usize = 2; + +/// Per-domain count of active, unretained peripherals holding it powered. +#[allow(clippy::declare_interior_mutable_const)] +const ZERO: AtomicU32 = AtomicU32::new(0); +static LOCKS: [AtomicU32; DOMAIN_COUNT] = [ZERO; DOMAIN_COUNT]; + +/// A guard that keeps `domain` powered across light sleep while held (degrading +/// to clock-gating), without preventing sleep like a `WakeLock` would. +pub(crate) struct PowerDomainLock { + domain: Domain, +} + +impl PowerDomainLock { + /// Keep `domain` powered until the guard is dropped. + pub(crate) fn new(domain: Domain) -> Self { + LOCKS[domain as usize].fetch_add(1, Ordering::AcqRel); + Self { domain } + } +} + +impl Drop for PowerDomainLock { + fn drop(&mut self) { + LOCKS[self.domain as usize].fetch_sub(1, Ordering::AcqRel); + } +} + +/// Whether `domain` may be powered down. On the C6 powering `TOP` down also +/// tears down the CPU domain, so `Top` requires both to be free. +pub(crate) fn can_power_down(domain: Domain) -> bool { + let blocked = match domain { + Domain::Cpu => LOCKS[Domain::Cpu as usize].load(Ordering::Acquire) != 0, + Domain::Top => { + LOCKS[Domain::Top as usize].load(Ordering::Acquire) != 0 + || LOCKS[Domain::Cpu as usize].load(Ordering::Acquire) != 0 + } + }; + !blocked +} diff --git a/esp-hal/src/rtc_cntl/retention.rs b/esp-hal/src/rtc_cntl/retention.rs index d3b18ab50e9..d0a6e72b63b 100644 --- a/esp-hal/src/rtc_cntl/retention.rs +++ b/esp-hal/src/rtc_cntl/retention.rs @@ -1,33 +1,28 @@ -//! # Register DMA (regDMA) based register retention +//! Register DMA (regDMA) based register retention (ESP32-C6). //! -//! ## Overview +//! The PAU's regDMA engine backs peripheral registers up to RAM and restores +//! them while the `TOP` domain is powered down in light sleep. It walks a linked +//! list of [`RegdmaLink`] nodes over PAU entry link 0 with no CPU involvement. +//! `sys_periph` builds the core register set into a caller-owned +//! [`SystemRetentionMemory`]; [`enable_top_retention`] chains it with any opt-in +//! peripheral entries before arming the link. Without a `SystemRetentionMemory` +//! the `TOP` domain is only clock-gated. //! -//! ESP32-C6 contains a **Power Assist Unit (PAU)** with a **regDMA** engine that -//! can automatically back up and restore peripheral/CPU register state to and -//! from RAM. ESP-IDF uses this engine to retain register contents while a power -//! domain (e.g. the CPU or the digital `TOP` domain) is powered down during -//! light sleep, so that execution can resume seamlessly after wakeup. -//! -//! regDMA walks a linked list of *nodes* stored in RAM. Each node describes one -//! backup/restore operation ([`RegdmaLink`]): CONTINUOUS (a run of registers via -//! a RAM buffer), ADDR_MAP (a run of registers where a bitmap selects which ones -//! to transfer), WRITE (a masked register write) or WAIT (poll a register). -//! -//! The list is executed by the **PMU auto-trigger** over PAU entry link 0: when -//! the digital `TOP` domain is powered down during light sleep the PMU runs the -//! list to back up the registers on the way into sleep and restore them on -//! wakeup, with no CPU involvement. The `sys_periph` module builds the -//! TOP-domain register set to retain, and [`enable_top_retention`] arms it. -//! -//! References (ESP-IDF `v5.4`): -//! - `components/soc/include/soc/regdma.h` (node layout) -//! - `components/hal/esp32c6/include/hal/pau_ll.h` -//! - `components/hal/esp32c6/pau_hal.c` -//! - `components/esp_hw_support/port/pau_regdma.c` +//! References (ESP-IDF `v5.4`): `soc/regdma.h`, `hal/esp32c6/pau_ll.h`, +//! `hal/esp32c6/pau_hal.c`, `esp_hw_support/port/pau_regdma.c`. + +use core::{ + marker::PhantomData, + ptr::NonNull, + sync::atomic::{Ordering, fence}, +}; -use core::sync::atomic::{Ordering, fence}; +use esp_sync::NonReentrantMutex; -use crate::peripherals::{PAU, PCR, PMU}; +use crate::{ + peripherals::{PAU, PCR, PMU}, + rtc_cntl::power_domain::{Domain, PowerDomainLock}, +}; // Bit layout of `regdma_link_head_t` (see ESP-IDF `regdma.h`): // https://github.com/espressif/esp-idf/blob/v5.4/components/soc/include/soc/regdma.h#L114-L123 @@ -46,42 +41,30 @@ enum LinkMode { /// Back up/restore a run of registers via a RAM buffer, where a 4-word /// bitmap selects which registers in the window are actually transferred /// (skipping e.g. read-only status/FIFO registers interspersed in a block). - AddrMap = 1, + AddrMap = 1, /// Unconditionally write a masked value to a register. - Write = 2, + Write = 2, /// Poll a register until `(reg & mask) == value`. - Wait = 3, + Wait = 3, } -/// A single regDMA linked-list node. -/// -/// The in-memory layout must match what the PAU hardware expects: the hardware -/// link address points at the `head` field, followed by the four body words. -/// The software-only `stat` block that ESP-IDF keeps *before* `head` is not -/// needed here, so it is omitted. -/// -/// The CONTINUOUS and WRITE/WAIT node bodies are both four words; the ADDR_MAP -/// body adds a four-word register-selection bitmap. A single struct with a -/// trailing `map` array covers all four modes (the hardware reads only as many -/// body words as the mode requires and then follows `next`, so the unused -/// trailing words are harmless padding for the other modes). Branch nodes are -/// not implemented. +/// A single regDMA linked-list node (matches the PAU `head` + body layout). /// /// - CONTINUOUS: `w0 = backup addr`, `w1 = restore addr`, `w2 = RAM buffer`. /// - ADDR_MAP: as CONTINUOUS, plus `map` selecting which registers to transfer. -/// - WRITE/WAIT: `w0 = target addr`, `w1 = value`, `w2 = mask` (`mem`/`map` -/// unused). +/// - WRITE/WAIT: `w0 = target addr`, `w1 = value`, `w2 = mask`. #[repr(C, align(4))] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub(crate) struct RegdmaLink { /// Packed `regdma_link_head_t`. head: u32, - /// Pointer to the next node's `head`, or `0` for the end of the list. + /// Next node's `head`, or `0` at the end of the list. next: u32, w0: u32, w1: u32, w2: u32, - /// ADDR_MAP register-selection bitmap; zero (and unread) for other modes. + /// ADDR_MAP register-selection bitmap; unread for other modes. map: [u32; 4], } @@ -111,10 +94,8 @@ impl RegdmaLink { Self::continuous_split(reg, reg, storage, len) } - /// A CONTINUOUS node whose backup source (`backup`) and restore destination - /// (`restore`) registers differ, sharing one RAM buffer. Used where hardware - /// exposes a value register for readback and a separate load register for - /// restore (e.g. the SysTimer counter). + /// A CONTINUOUS node with distinct backup/restore registers sharing one RAM + /// buffer (e.g. the SysTimer value vs. load registers). fn continuous_split(backup: u32, restore: u32, storage: u32, len: u32) -> Self { Self { head: Self::head(LinkMode::Continuous, len, false, false), @@ -126,14 +107,9 @@ impl RegdmaLink { } } - /// An ADDR_MAP node backing up/restoring the `count` registers selected by - /// `map` from the register window starting at `reg`, via `storage`. - /// - /// `map` is a bitmap over the window (bit `i` = the register at - /// `reg + i * 4`); the engine transfers `count` registers total (one per - /// set bit, walking bits from LSB up) into `count` consecutive words of - /// `storage`. Used to skip read-only/FIFO registers interspersed in a - /// peripheral's register block (e.g. the console UART). + /// An ADDR_MAP node: back up/restore the `count` registers selected by `map` + /// (bit `i` = register at `reg + i * 4`) from `reg` into `storage`, skipping + /// interspersed read-only/FIFO registers. fn addr_map(reg: u32, storage: u32, count: u32, map: [u32; 4]) -> Self { Self { head: Self::head(LinkMode::AddrMap, count, false, false), @@ -145,10 +121,8 @@ impl RegdmaLink { } } - /// A WRITE node that writes `value` (under `mask`) to `target`. - /// - /// `skip_b`/`skip_r` select whether the write happens during backup and/or - /// restore (WRITE/WAIT nodes are usually restore-only or backup-only). + /// A WRITE node that writes `value` (under `mask`) to `target`. `skip_b`/ + /// `skip_r` gate the write on the backup/restore pass. fn write(target: u32, value: u32, mask: u32, skip_b: bool, skip_r: bool) -> Self { Self { head: Self::head(LinkMode::Write, 0, skip_b, skip_r), @@ -177,65 +151,435 @@ impl RegdmaLink { } } -/// Chain a slice of nodes into a single linked list: each node's `next` points -/// at the following node's `head`, and only the last node keeps its EOF flag. -/// Returns the head address to program into the PAU. -fn link_nodes(nodes: &mut [RegdmaLink]) -> u32 { +// Console UART config-register retention, shared by the always-on console and +// the opt-in `UartRetentionMemory`. ESP-IDF v5.4 `uart_periph.c` +// `UART_SLEEP_RETENTION_ENTRIES`, `uart_reg.h`. +const UART_INT_ENA_OFF: u32 = 0x0C; // UART_INT_ENA_REG +const UART_REG_UPDATE_OFF: u32 = 0x98; // UART_REG_UPDATE_REG +const UART_REG_UPDATE: u32 = 1 << 0; +/// Registers retained (set bits in [`UART_REGS_MAP`]). +const UART_RETENTION_REGS_CNT: u32 = 21; +/// `uart_regs_map[4]`: config registers in the INT_ENA..ID window. +const UART_REGS_MAP: [u32; 4] = [0x007f_ff6d, 0x0000_0010, 0, 0]; +/// One ADDR_MAP + a restore-only WRITE+WAIT pulsing `UART_REG_UPDATE`. +const UART_NODE_COUNT: usize = 3; + +/// Build the UART retention sequence for `base` into `nodes`, backing the +/// registers up into `storage`. The WRITE+WAIT pulse the update bit on restore +/// to latch the shadow (`_SYNC`) registers. +fn build_uart_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { + nodes[0] = RegdmaLink::addr_map( + base + UART_INT_ENA_OFF, + storage, + UART_RETENTION_REGS_CNT, + UART_REGS_MAP, + ); + nodes[1] = RegdmaLink::write( + base + UART_REG_UPDATE_OFF, + UART_REG_UPDATE, + UART_REG_UPDATE, + true, + false, + ); + nodes[2] = RegdmaLink::wait(base + UART_REG_UPDATE_OFF, 0, UART_REG_UPDATE, true, false); +} + +// I2C config-register retention. ESP-IDF v5.4 `i2c_periph.c` +// `i2c0_regs_retention`, `i2c_reg.h`. Config registers are shadowed, so restore +// pulses the FSM reset then requests a config update and waits for it to latch. +const I2C_SCL_LOW_PERIOD_OFF: u32 = 0x00; // I2C_SCL_LOW_PERIOD_REG: ADDR_MAP window base +const I2C_CTR_OFF: u32 = 0x04; // I2C_CTR_REG +const I2C_FSM_RST: u32 = 1 << 10; // I2C_FSM_RST (value == mask) +const I2C_CONF_UPGATE: u32 = 1 << 11; // I2C_CONF_UPGATE (value == mask) +/// Registers retained (set bits in [`I2C_REGS_MAP`]). +const I2C_RETENTION_REGS_CNT: u32 = 18; +/// `i2c0_regs_map[4]`: config registers in the `SCL_LOW_PERIOD..SCL_STRETCH_CONF` window. +const I2C_REGS_MAP: [u32; 4] = [0xc03f_345b, 0x3, 0, 0]; +/// One ADDR_MAP + a restore-only WRITE*3/WAIT pulsing `FSM_RST` then `CONF_UPGATE`. +const I2C_NODE_COUNT: usize = 5; + +/// Build the I2C retention sequence for `base` into `nodes`, backing the +/// registers up into `storage`. +fn build_i2c_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { + let ctr = base + I2C_CTR_OFF; + nodes[0] = RegdmaLink::addr_map( + base + I2C_SCL_LOW_PERIOD_OFF, + storage, + I2C_RETENTION_REGS_CNT, + I2C_REGS_MAP, + ); + // Restore-only: pulse FSM reset, request config update, wait for it to latch. + nodes[1] = RegdmaLink::write(ctr, I2C_FSM_RST, I2C_FSM_RST, true, false); + nodes[2] = RegdmaLink::write(ctr, 0, I2C_FSM_RST, true, false); + nodes[3] = RegdmaLink::write(ctr, I2C_CONF_UPGATE, I2C_CONF_UPGATE, true, false); + nodes[4] = RegdmaLink::wait(ctr, 0, I2C_CONF_UPGATE, true, false); +} + +// GPSPI2 config-register retention. ESP-IDF v5.4 `spi_periph.c` +// `spi2_regs_retention`, `spi_reg.h`. IDF's restore-time re-set of the +// TRANS_DONE/DMA_SEG_TRANS_DONE interrupt bits is omitted: esp-hal only powers +// `TOP` down on an idle bus, so it would only inject a spurious completion IRQ. +const SPI_CMD_OFF: u32 = 0x00; // SPI_CMD_REG: ADDR_MAP window base +/// Registers retained (set bits in [`SPI_REGS_MAP`]). +const SPI_RETENTION_REGS_CNT: u32 = 12; +/// `spi_regs_map[4]`: config registers in the `CMD..SLAVE` window. +const SPI_REGS_MAP: [u32; 4] = [0x0000_31ff, 0x0100_0000, 0, 0]; +/// A single ADDR_MAP over the config registers. +const SPI_NODE_COUNT: usize = 1; + +/// Build the SPI retention sequence for `base` into `nodes`, backing the +/// registers up into `storage`. +fn build_spi_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { + nodes[0] = RegdmaLink::addr_map( + base + SPI_CMD_OFF, + storage, + SPI_RETENTION_REGS_CNT, + SPI_REGS_MAP, + ); +} + +/// A node in the intrusive registry of opt-in peripheral retention sequences. +/// +/// One lives inside each peripheral's caller-owned retention memory, so any +/// number of peripherals can register without a fixed table or allocation. The +/// pointers are only dereferenced in [`arm_link`] (under [`REGISTRY`], on the +/// single HP core), pointing at borrow-frozen memory until deregistered. +pub(crate) struct RetentionNode { + next: Option>, + head: *mut RegdmaLink, + len: usize, +} + +impl RetentionNode { + const fn new() -> Self { + Self { + next: None, + head: core::ptr::null_mut(), + len: 0, + } + } +} + +impl core::fmt::Debug for RetentionNode { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("RetentionNode") + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for RetentionNode { + fn format(&self, fmt: defmt::Formatter<'_>) { + defmt::write!(fmt, "RetentionNode") + } +} + +/// Head of the intrusive list of registered peripheral retention sequences. +struct Registry(Option>); + +// SAFETY: the pointers are only followed under the `REGISTRY` lock, and while +// armed they point at borrow-frozen caller memory on the single HP core. +unsafe impl Send for Registry {} + +static REGISTRY: NonReentrantMutex = NonReentrantMutex::new(Registry(None)); + +/// Push `node` onto the registry so [`arm_link`] chains its `nodes` on the next +/// TOP power-down. +fn register_node(node: &mut RetentionNode, nodes: &mut [RegdmaLink]) -> NonNull { + node.head = nodes.as_mut_ptr(); + node.len = nodes.len(); + REGISTRY.with(|registry| { + node.next = registry.0; + registry.0 = Some(NonNull::from(&mut *node)); + }); + NonNull::from(node) +} + +/// Remove a registration previously made with [`register_node`]. +fn deregister_node(node: &mut RetentionNode) { + let target = NonNull::from(&mut *node); + REGISTRY.with(|registry| { + let mut link: *mut Option> = &mut registry.0; + // SAFETY: every pointer in the list points at a live, registered node; + // the walk only follows `next` links until it reaches `target`. + unsafe { + while let Some(current) = *link { + if current == target { + *link = (*current.as_ptr()).next; + break; + } + link = &raw mut (*current.as_ptr()).next; + } + } + }); + node.next = None; +} + +/// Chain a slice of nodes (clearing each EOF flag), returning the last node so +/// the caller can terminate it or link it to a following segment. +fn link_internal(nodes: &mut [RegdmaLink]) -> *mut RegdmaLink { let len = nodes.len(); - for i in 0..len { - if i + 1 < len { - nodes[i].next = nodes[i + 1].addr(); - nodes[i].head &= !HEAD_EOF_BIT; - } else { - nodes[i].next = 0; - nodes[i].head |= HEAD_EOF_BIT; + for i in 0..len - 1 { + nodes[i].next = nodes[i + 1].addr(); + nodes[i].head &= !HEAD_EOF_BIT; + } + &mut nodes[len - 1] +} + +/// Chain the always-retained `core` list plus every registered opt-in entry +/// into one list, terminating only the final node. Returns the head address to +/// program into the PAU. +fn arm_link(core: &mut [RegdmaLink]) -> u32 { + let head = core[0].addr(); + let mut tail = link_internal(core); + + REGISTRY.with(|registry| { + let mut current = registry.0; + while let Some(node) = current { + // SAFETY: a registered node points at a live, borrow-frozen + // caller-owned array of `len` nodes, distinct from every other. + let (seg_head, seg_len, next) = unsafe { + let node = node.as_ref(); + (node.head, node.len, node.next) + }; + let seg = unsafe { core::slice::from_raw_parts_mut(seg_head, seg_len) }; + // SAFETY: `tail` is the last node of the previous segment. + unsafe { + (*tail).next = seg[0].addr(); + (*tail).head &= !HEAD_EOF_BIT; + } + tail = link_internal(seg); + current = next; } + }); + + // Terminate the final node. + // SAFETY: `tail` points at the last node of the last segment. + unsafe { + (*tail).next = 0; + (*tail).head |= HEAD_EOF_BIT; } - nodes[0].addr() + head } -/// Arm PMU-driven regDMA retention of the TOP-domain system peripherals for the -/// upcoming light sleep. +/// Generate a per-peripheral caller-owned regDMA backing store, sized to its +/// `nodes` and register `buf`. `$build` is its sequence builder. +macro_rules! peripheral_retention_memory { + ($name:ident, $nodes:expr, $words:expr, $build:path, $doc:expr) => { + #[doc = $doc] + #[instability::unstable] + #[derive(Debug)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[repr(C, align(4))] + pub struct $name { + node: RetentionNode, + nodes: [RegdmaLink; $nodes], + buf: [u32; $words], + } + + #[instability::unstable] + impl Default for $name { + fn default() -> Self { + Self::new() + } + } + + impl $name { + #[doc = concat!("Create an empty [`", stringify!($name), "`].")] + #[instability::unstable] + pub const fn new() -> Self { + Self { + node: RetentionNode::new(), + nodes: [RegdmaLink::EMPTY; $nodes], + buf: [0; $words], + } + } + } + + impl RetentionMemory for $name { + fn register(&mut self, base: u32) -> NonNull { + let storage = self.buf.as_mut_ptr() as u32; + $build(base, &mut self.nodes, storage); + register_node(&mut self.node, &mut self.nodes) + } + } + }; +} + +peripheral_retention_memory!( + UartRetentionMemory, + UART_NODE_COUNT, + UART_RETENTION_REGS_CNT as usize, + build_uart_seq, + "Caller-owned backing store retaining one UART's config registers across a \ +`TOP` power-down. Passed to \ +[`Uart::with_retention_memory`](crate::uart::Uart::with_retention_memory); the \ +console/log UART is retained automatically." +); + +peripheral_retention_memory!( + I2cRetentionMemory, + I2C_NODE_COUNT, + I2C_RETENTION_REGS_CNT as usize, + build_i2c_seq, + "Caller-owned backing store retaining one I2C's config registers across a \ +`TOP` power-down. Passed to \ +[`I2c::with_retention_memory`](crate::i2c::master::I2c::with_retention_memory). \ +See [`UartRetentionMemory`]." +); + +peripheral_retention_memory!( + SpiRetentionMemory, + SPI_NODE_COUNT, + SPI_RETENTION_REGS_CNT as usize, + build_spi_seq, + "Caller-owned backing store retaining one SPI's config registers across a \ +`TOP` power-down. Passed to \ +[`Spi::with_retention_memory`](crate::spi::master::Spi::with_retention_memory). \ +See [`UartRetentionMemory`]." +); + +/// Caller-owned retention memory that can be registered for TOP-domain +/// retention. Implemented by the generated `*RetentionMemory` types. +pub(crate) trait RetentionMemory { + /// Build the retention sequence for `base` and register it, returning its + /// registry node. + fn register(&mut self, base: u32) -> NonNull; +} + +/// A `TOP`-domain peripheral driver's power-management state: either active and +/// holding a [`PowerDomainLock`] (keeping `TOP` powered so it can't lose state, +/// without preventing sleep), or retained (the lock is dropped and regDMA +/// save/restores its config across a `TOP` power-down from `'d`-borrowed memory). +/// Stored in the driver, so the user never juggles a separate guard. +pub(crate) enum PowerManagement<'d, M: RetentionMemory> { + /// Active, not retained: the held lock keeps `TOP` powered. + PowerDomainLock { _lock: PowerDomainLock }, + /// Retained: `node` points into the caller-owned memory borrowed for `'d`. + Retain { + node: NonNull, + _mem: PhantomData<&'d mut M>, + }, +} + +impl<'d, M: RetentionMemory> PowerManagement<'d, M> { + /// Active, un-retained: keep `TOP` powered so sleep can't lose its state. + pub(crate) fn new() -> Self { + Self::PowerDomainLock { + _lock: PowerDomainLock::new(Domain::Top), + } + } + + /// Opt into retention: register `mem` for `base` and drop the domain lock so + /// a `TOP` power-down can take effect. + pub(crate) fn retain(&mut self, mem: &'d mut M, base: u32) { + let node = mem.register(base); + *self = Self::Retain { + node, + _mem: PhantomData, + }; + } +} + +impl Drop for PowerManagement<'_, M> { + fn drop(&mut self) { + if let Self::Retain { node, .. } = self { + // SAFETY: the node lives in caller memory borrowed for `'d`, which + // outlives `self`, so it is still valid to unlink here. + unsafe { deregister_node(node.as_mut()) }; + } + } +} + +// SAFETY: the only thread-unsafe state a `Retain` holds is the raw node/link +// pointers, and those are only ever dereferenced on the single HP core under the +// `REGISTRY` mutex (see `arm_link`/`deregister_node`); the owner never follows +// them otherwise. +unsafe impl Send for PowerManagement<'_, M> {} +unsafe impl Sync for PowerManagement<'_, M> {} + +impl core::fmt::Debug for PowerManagement<'_, M> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::PowerDomainLock { .. } => f.write_str("PowerManagement::PowerDomainLock"), + Self::Retain { .. } => f.write_str("PowerManagement::Retain"), + } + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for PowerManagement<'_, M> { + fn format(&self, fmt: defmt::Formatter<'_>) { + match self { + Self::PowerDomainLock { .. } => defmt::write!(fmt, "PowerManagement::PowerDomainLock"), + Self::Retain { .. } => defmt::write!(fmt, "PowerManagement::Retain"), + } + } +} + +/// Caller-owned backing store for the ESP32-C6 TOP-domain system-peripheral +/// register set (PCR, interrupt matrix, HP system, TEE/APM, IO MUX, GPIO matrix, +/// flash SPI mem, console UART and SysTimer). /// -/// When the PMU powers down the digital `TOP` domain (`pd_top`) it loses the -/// system-peripheral register state, so it must be backed up on the -/// HP_ACTIVE -> HP_SLEEP transition and restored on HP_SLEEP -> HP_ACTIVE. Both -/// transitions run PAU entry link 0 (the direction is chosen by the PMU), so a -/// single combined list (see the `sys_periph` module) serves both. +/// The core state regDMA must retain for the `TOP` domain to power down at all; +/// the caller opts in via [`RtcSleepConfig::with_top_power_down`]. Individual +/// peripherals opt into retaining their own config via `with_retention_memory`. /// -/// This programs the entry-link address and enables the two backup phases. The -/// backup *mode*/direction and clocks are already configured per HP state by the -/// sleep power config; only the enable bits (reset every sleep) are flipped -/// here. Mirrors ESP-IDF `sleep_retention` link setup + -/// `pmu_sleep_enable_regdma_backup()` (active/sleep phases only, as there is no -/// modem state). +/// [`RtcSleepConfig::with_top_power_down`]: crate::rtc_cntl::sleep::RtcSleepConfig::with_top_power_down +#[instability::unstable] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(C, align(4))] +pub struct SystemRetentionMemory { + nodes: [RegdmaLink; sys_periph::NODE_COUNT], + buf: [u32; sys_periph::BUF_WORDS], +} + +#[instability::unstable] +impl Default for SystemRetentionMemory { + fn default() -> Self { + Self::new() + } +} + +impl SystemRetentionMemory { + /// Create a new, zeroed system-peripheral retention buffer. + #[instability::unstable] + pub const fn new() -> Self { + Self { + nodes: [RegdmaLink::EMPTY; sys_periph::NODE_COUNT], + buf: [0; sys_periph::BUF_WORDS], + } + } +} + +/// Arm regDMA retention of the TOP-domain peripherals for the upcoming light +/// sleep: program PAU entry link 0 and enable the backup phases. /// -/// Must be called after the PMU power config has been applied (which rewrites -/// the backup registers) and before the sleep request. -pub(crate) fn enable_top_retention() { - // pau_ll_enable_bus_clock(true): enable the regDMA bus clock and release its - // reset before programming the entry link. +/// Must be called after the PMU power config (which resets the backup-enable +/// bits) and before the sleep request. Rebuilds the chain from the live registry +/// every time, so the PAU never walks a deregistered entry. Mirrors ESP-IDF's +/// link setup + `pmu_sleep_enable_regdma_backup()` (active/sleep phases only). +pub(crate) fn enable_top_retention(mem: &mut SystemRetentionMemory) { + // pau_ll_enable_bus_clock: enable the regDMA bus clock and release its reset. PCR::regs().regdma_conf().modify(|_, w| { w.regdma_clk_en().set_bit(); w.regdma_rst_en().clear_bit() }); - // pau_hal_set_regdma_wait_timeout: bound how long a WAIT node polls a - // register so a never-satisfied condition can't hang the engine. Values - // match ESP-IDF's PAU_REGDMA_LINK_WAIT_{RETRY_COUNT,READ_INTERNAL}. + // pau_hal_set_regdma_wait_timeout: bound WAIT polling so a never-satisfied + // condition can't hang the engine (ESP-IDF PAU_REGDMA_LINK_WAIT_*). PAU::regs().regdma_bkp_conf().modify(|_, w| unsafe { w.link_tout_thres().bits(1000); w.read_interval().bits(32) }); - // Build the combined SYS_PERIPH list and program it as PAU entry link 0. - let head = link_nodes(sys_periph::build_link()); + // Build the SYS_PERIPH list plus opt-in entries and program it as link 0. + let head = arm_link(sys_periph::build_link(&mut mem.nodes, &mut mem.buf)); fence(Ordering::SeqCst); PAU::regs() .regdma_link_0_addr() .write(|w| unsafe { w.bits(head) }); - // pmu_sleep_enable_regdma_backup (active <-> sleep only): back up on - // active->sleep, restore on sleep->active. + // pmu_sleep_enable_regdma_backup: back up active->sleep, restore sleep->active. let pmu = PMU::regs(); pmu.hp_sleep_backup() .modify(|_, w| w.hp_active2sleep_backup_en().set_bit()); @@ -243,25 +587,21 @@ pub(crate) fn enable_top_retention() { .modify(|_, w| w.hp_sleep2active_backup_en().set_bit()); } -/// ESP32-C6 TOP-domain system-peripheral retention link. +/// ESP32-C6 TOP-domain system-peripheral retention link: the register regions +/// for the core system peripherals lost when `TOP` powers down, mirroring +/// ESP-IDF's `SLEEP_RETENTION_MODULE_SYS_PERIPH` + `..._CLOCK_SYSTEM` in +/// retention-priority order (system clock first). /// -/// When the digital `TOP` power domain is powered down during light sleep, the -/// registers of the core system peripherals are lost and must be regDMA-backed -/// up beforehand and restored on wakeup. This module builds the linked list -/// describing those register regions. -/// -/// The set and ordering mirror ESP-IDF's `SLEEP_RETENTION_MODULE_SYS_PERIPH` -/// plus `SLEEP_RETENTION_MODULE_CLOCK_SYSTEM` (both TOP-domain), sorted by the -/// same retention priority (system clock first). Within a link, ESP-IDF keeps -/// the same nodes for backup (entry 0) and restore (entry 2), so a single -/// combined list can be programmed into both PAU entry links. -/// -/// References (ESP-IDF `v5.4`): -/// - [`system_retention_periph.c`](https://github.com/espressif/esp-idf/blob/v5.4/components/soc/esp32c6/system_retention_periph.c) -/// - [`sleep_clock.c`](https://github.com/espressif/esp-idf/blob/v5.4/components/esp_hw_support/lowpower/port/esp32c6/sleep_clock.c) -/// - [`sleep_system_peripheral.c`](https://github.com/espressif/esp-idf/blob/v5.4/components/esp_hw_support/sleep_system_peripheral.c) +/// References (ESP-IDF `v5.4`): `soc/esp32c6/system_retention_periph.c`, +/// `esp_hw_support/.../sleep_clock.c`, `esp_hw_support/sleep_system_peripheral.c`. mod sys_periph { - use super::{HEAD_LENGTH_MASK, RegdmaLink}; + use super::{ + HEAD_LENGTH_MASK, + RegdmaLink, + UART_NODE_COUNT, + UART_RETENTION_REGS_CNT, + build_uart_seq, + }; /// TEE mode-control register, rewritten early on restore to unlock access. const TEE_M4_MODE_CTRL_REG: u32 = 0x6009_8010; @@ -272,44 +612,95 @@ mod sys_periph { count: u32, } - /// Continuous register regions to retain, in ESP-IDF retention-priority - /// order (highest priority first). The end registers used to size each - /// region are noted; counts are `((end - base) / 4) + 1`. + /// Continuous register regions to retain, in retention-priority order. The + /// sizing end register is noted per region; `count = ((end - base) / 4) + 1`. const CONT_REGIONS: &[ContRegion] = &[ // PRI_0 - system clock/reset (PCR) - ContRegion { base: 0x6009_6000, count: 79 }, // PCR base ..= PCR_SRAM_POWER_CONF_REG (+0x138) - ContRegion { base: 0x6009_6FF0, count: 1 }, // PCR_RESET_EVENT_BYPASS_REG + ContRegion { + base: 0x6009_6000, + count: 79, + }, // PCR base ..= PCR_SRAM_POWER_CONF_REG (+0x138) + ContRegion { + base: 0x6009_6FF0, + count: 1, + }, // PCR_RESET_EVENT_BYPASS_REG // PRI_4 - TEE/APM - ContRegion { base: 0x6009_9000, count: 68 }, // HP_APM base ..= HP_APM_CLOCK_GATE_REG (+0x10c) - ContRegion { base: 0x6009_8000, count: 33 }, // TEE base ..= TEE_CLOCK_GATE_REG (+0x80) + ContRegion { + base: 0x6009_9000, + count: 68, + }, // HP_APM base ..= HP_APM_CLOCK_GATE_REG (+0x10c) + ContRegion { + base: 0x6009_8000, + count: 33, + }, // TEE base ..= TEE_CLOCK_GATE_REG (+0x80) // PRI_5 - interrupt matrix + HP system - ContRegion { base: 0x6001_0000, count: 81 }, // INTMTX base ..= INTMTX_CORE0_CLOCK_GATE_REG (+0x140) - ContRegion { base: 0x6009_5000, count: 18 }, // HP_SYSTEM base ..= HP_SYSTEM_MEM_TEST_CONF_REG (+0x44) + ContRegion { + base: 0x6001_0000, + count: 81, + }, // INTMTX base ..= INTMTX_CORE0_CLOCK_GATE_REG (+0x140) + ContRegion { + base: 0x6009_5000, + count: 18, + }, // HP_SYSTEM base ..= HP_SYSTEM_MEM_TEST_CONF_REG (+0x44) // PRI_6 - IO MUX + GPIO matrix - ContRegion { base: 0x6009_0000, count: 32 }, // IO_MUX base ..= IO_MUX_GPIO30_REG (+0x7c) - ContRegion { base: 0x6009_1554, count: 35 }, // GPIO_FUNC0_OUT_SEL ..= GPIO_FUNC34_OUT_SEL - ContRegion { base: 0x6009_114C, count: 127 }, // GPIO_STATUS_NEXT ..= GPIO_FUNC124_IN_SEL - ContRegion { base: 0x6009_1000, count: 64 }, // GPIO base ..= GPIO_PIN34_REG (+0xfc) + ContRegion { + base: 0x6009_0000, + count: 32, + }, // IO_MUX base ..= IO_MUX_GPIO30_REG (+0x7c) + ContRegion { + base: 0x6009_1554, + count: 35, + }, // GPIO_FUNC0_OUT_SEL ..= GPIO_FUNC34_OUT_SEL + ContRegion { + base: 0x6009_114C, + count: 127, + }, // GPIO_STATUS_NEXT ..= GPIO_FUNC124_IN_SEL + ContRegion { + base: 0x6009_1000, + count: 64, + }, // GPIO base ..= GPIO_PIN34_REG (+0xfc) // PRI_6 - Flash SPI mem (SPIMEM1 then SPIMEM0). MMU content/index // registers are intentionally excluded (see ESP-IDF note). - ContRegion { base: 0x6000_3000, count: 55 }, // SPIMEM1 base ..= SPI_MEM_SPI_SMEM_DDR (+0xd8) - ContRegion { base: 0x6000_3100, count: 41 }, // SPIMEM1 FMEM_PMS0_ATTR ..= SMEM_AC (+0x1a0) - ContRegion { base: 0x6000_3200, count: 1 }, // SPIMEM1 CLOCK_GATE - ContRegion { base: 0x6000_3384, count: 31 }, // SPIMEM1 MMU_POWER_CTRL ..= DATE (+0x3fc) - ContRegion { base: 0x6000_2000, count: 55 }, // SPIMEM0 base ..= SPI_MEM_SPI_SMEM_DDR - ContRegion { base: 0x6000_2100, count: 41 }, // SPIMEM0 FMEM_PMS0_ATTR ..= SMEM_AC - ContRegion { base: 0x6000_2200, count: 1 }, // SPIMEM0 CLOCK_GATE - ContRegion { base: 0x6000_2384, count: 31 }, // SPIMEM0 MMU_POWER_CTRL ..= DATE + ContRegion { + base: 0x6000_3000, + count: 55, + }, // SPIMEM1 base ..= SPI_MEM_SPI_SMEM_DDR (+0xd8) + ContRegion { + base: 0x6000_3100, + count: 41, + }, // SPIMEM1 FMEM_PMS0_ATTR ..= SMEM_AC (+0x1a0) + ContRegion { + base: 0x6000_3200, + count: 1, + }, // SPIMEM1 CLOCK_GATE + ContRegion { + base: 0x6000_3384, + count: 31, + }, // SPIMEM1 MMU_POWER_CTRL ..= DATE (+0x3fc) + ContRegion { + base: 0x6000_2000, + count: 55, + }, // SPIMEM0 base ..= SPI_MEM_SPI_SMEM_DDR + ContRegion { + base: 0x6000_2100, + count: 41, + }, // SPIMEM0 FMEM_PMS0_ATTR ..= SMEM_AC + ContRegion { + base: 0x6000_2200, + count: 1, + }, // SPIMEM0 CLOCK_GATE + ContRegion { + base: 0x6000_2384, + count: 31, + }, // SPIMEM0 MMU_POWER_CTRL ..= DATE ]; - /// Index in [`CONT_REGIONS`] at which the TEE/APM (PRI_4) group starts; the - /// PRI_2 TEE-critical WRITE node is inserted just before it. + /// [`CONT_REGIONS`] index where TEE/APM (PRI_4) starts; the PRI_2 TEE WRITE + /// node is inserted just before it. const TEE_APM_START: usize = 2; - /// Index in [`CONT_REGIONS`] at which the IO MUX / GPIO (PRI_6) group - /// starts. The console-UART (PRI_5) nodes are inserted just before it, so - /// the continuous regions split into a PRI_4/5 prefix (TEE/APM, interrupt - /// matrix, HP system) and a PRI_6 suffix (IO MUX, GPIO, SPI mem). + /// [`CONT_REGIONS`] index where IO MUX/GPIO (PRI_6) starts; the console-UART + /// (PRI_5) nodes are inserted just before it. const IOMUX_START: usize = 6; const fn total_words() -> usize { @@ -322,24 +713,10 @@ mod sys_periph { words } - // Console UART0 (base 0x6000_0000). Retained via an ADDR_MAP node whose - // bitmap selects the 21 configuration registers out of the 37-register - // window between UART_INT_ENA_REG (+0x0c) and UART_ID_REG (+0x9c), skipping - // the interspersed FIFO/status/interrupt-raw registers, followed by a - // restore-only WRITE+WAIT that pulses UART_REG_UPDATE to load the shadow - // (`_SYNC`) registers. Values from ESP-IDF v5.4 `uart_periph.c` - // `UART_SLEEP_RETENTION_ENTRIES` and `uart_reg.h`. - const UART_INT_ENA_REG: u32 = 0x6000_000C; - const UART_REG_UPDATE_REG: u32 = 0x6000_0098; - const UART_REG_UPDATE: u32 = 1 << 0; - /// Number of registers actually retained (set bits in `UART_REGS_MAP`). - const UART_RETENTION_REGS_CNT: u32 = 21; - /// `uart_regs_map[4]` from ESP-IDF: bitmap over the INT_ENA..ID window. - const UART_REGS_MAP: [u32; 4] = [0x007f_ff6d, 0x0000_0010, 0, 0]; - const UART_NODE_COUNT: usize = 3; - - // SysTimer (base 0x6000_A000). Register offsets and bitfield masks from - // ESP-IDF v5.4 `systimer_reg.h`; node sequence from + /// Console UART0 base, retained automatically via [`build_uart_seq`]. + const UART0_BASE: u32 = 0x6000_0000; + + // SysTimer. Offsets/masks from ESP-IDF v5.4 `systimer_reg.h`; sequence from // `systimer_regs_retention[]`. const ST_BASE: u32 = 0x6000_A000; const ST_CONF: u32 = ST_BASE; // +0x00 @@ -368,38 +745,31 @@ mod sys_periph { const ST_TARGETS_LEN: u32 = 9; const SYSTIMER_NODE_COUNT: usize = 19; - /// SysTimer CONTINUOUS-node words: unit0/1 value (2+2), targets (9), - /// conf (1) and int_ena (1). + /// SysTimer CONTINUOUS words: unit0/1 value (2+2), targets (9), conf, int_ena. const SYSTIMER_CONT_WORDS: usize = 2 + 2 + ST_TARGETS_LEN as usize + 1 + 1; - /// One node per continuous region, the TEE-critical WRITE node, the console - /// UART sequence, and the SysTimer sequence. - const NODE_COUNT: usize = CONT_REGIONS.len() + 1 + UART_NODE_COUNT + SYSTIMER_NODE_COUNT; - const BUF_WORDS: usize = + /// One node per continuous region + TEE WRITE + console UART + SysTimer. + pub(super) const NODE_COUNT: usize = + CONT_REGIONS.len() + 1 + UART_NODE_COUNT + SYSTIMER_NODE_COUNT; + pub(super) const BUF_WORDS: usize = total_words() + UART_RETENTION_REGS_CNT as usize + SYSTIMER_CONT_WORDS; - static mut NODES: [RegdmaLink; NODE_COUNT] = [RegdmaLink::EMPTY; NODE_COUNT]; - static mut BUF: [u32; BUF_WORDS] = [0; BUF_WORDS]; - - // Every region count must fit the 10-bit `length` field of a regDMA node. + // Every region count must fit the 10-bit node `length` field. const _: () = { let mut i = 0; while i < CONT_REGIONS.len() { - assert!(CONT_REGIONS[i].count <= HEAD_LENGTH_MASK); + core::assert!(CONT_REGIONS[i].count <= HEAD_LENGTH_MASK); i += 1; } }; - /// (Re)build the SYS_PERIPH retention linked list into static storage and - /// return the node slice ready to be chained/triggered. - /// - /// Rebuilding on each call keeps the nodes' `next`/buffer pointers - /// self-consistent and is cheap (a few dozen writes). - pub(super) fn build_link() -> &'static mut [RegdmaLink] { - // SAFETY: retention is driven from a single context around sleep; there - // is no concurrent access to these statics. - let nodes = unsafe { &mut *core::ptr::addr_of_mut!(NODES) }; - let buf_base = core::ptr::addr_of_mut!(BUF) as *mut u32; + /// (Re)build the SYS_PERIPH retention list into `nodes`/`buf` and return the + /// filled node slice. + pub(super) fn build_link<'a>( + nodes: &'a mut [RegdmaLink; NODE_COUNT], + buf: &mut [u32; BUF_WORDS], + ) -> &'a mut [RegdmaLink] { + let buf_base = buf.as_mut_ptr(); let mut node = 0; let mut word = 0; @@ -412,8 +782,8 @@ mod sys_periph { node += 1; } - // PRI_2: TEE-critical WRITE node (restore-only: skip on backup). Clears - // TEE_M4_MODE_CTRL so the following TEE/APM restore can write freely. + // PRI_2: restore-only WRITE clearing TEE_M4_MODE_CTRL so the TEE/APM + // restore can write freely. nodes[node] = RegdmaLink::write(TEE_M4_MODE_CTRL_REG, 0, 0xFFFF_FFFF, true, false); node += 1; @@ -425,29 +795,11 @@ mod sys_periph { node += 1; } - // PRI_5: console UART0. ADDR_MAP restores the config registers, then a - // restore-only WRITE+WAIT pulses UART_REG_UPDATE to latch the shadow - // registers. The WRITE/WAIT skip the backup pass (they only matter on - // restore), so the backup just reads the selected registers. + // PRI_5: console UART0 (same sequence as the opt-in path). let mem = unsafe { buf_base.add(word) } as u32; - nodes[node] = RegdmaLink::addr_map( - UART_INT_ENA_REG, - mem, - UART_RETENTION_REGS_CNT, - UART_REGS_MAP, - ); + build_uart_seq(UART0_BASE, &mut nodes[node..node + UART_NODE_COUNT], mem); word += UART_RETENTION_REGS_CNT as usize; - node += 1; - nodes[node] = RegdmaLink::write( - UART_REG_UPDATE_REG, - UART_REG_UPDATE, - UART_REG_UPDATE, - true, - false, - ); - node += 1; - nodes[node] = RegdmaLink::wait(UART_REG_UPDATE_REG, 0, UART_REG_UPDATE, true, false); - node += 1; + node += UART_NODE_COUNT; // PRI_6: IO MUX, GPIO matrix, SPI mem. for region in &CONT_REGIONS[IOMUX_START..] { @@ -458,14 +810,9 @@ mod sys_periph { } // PRI_6: SysTimer. Backup latches each unit's counter (UPDATE + wait for - // VALUE_VALID) and reads the value; restore writes the value into the - // LOAD registers and triggers a load. The counter value is read from the - // VALUE_HI/LO registers but restored into the LOAD_HI/LO registers, so - // those CONTINUOUS nodes use split backup/restore addresses. - // - // `alloc` reserves `len` words of buffer and returns their address; it - // captures only the (copyable) buffer base, leaving `nodes` free to - // index directly. + // VALUE_VALID) and reads it; restore loads it back and triggers a load. + // The value is read from VALUE_HI/LO but restored into LOAD_HI/LO, hence + // the split backup/restore addresses. let alloc = |len: u32, word: &mut usize| -> u32 { let mem = unsafe { buf_base.add(*word) } as u32; *word += len as usize; @@ -514,8 +861,7 @@ mod sys_periph { nodes[node] = RegdmaLink::write(comp, ST_COMP_LOAD, ST_COMP_LOAD, true, false); node += 1; } - // Re-arm period mode: clear then set for target0/1, clear for target2 - // (matches ESP-IDF's write sequence). + // Re-arm period mode: clear+set for target0/1, clear for target2. for target in [ST_TARGET0_CONF, ST_TARGET1_CONF] { nodes[node] = RegdmaLink::write(target, 0, ST_TARGET_PERIOD_MODE, true, false); node += 1; diff --git a/esp-hal/src/rtc_cntl/sleep/esp32c6.rs b/esp-hal/src/rtc_cntl/sleep/esp32c6.rs index c386a1c1361..acb37fcd241 100644 --- a/esp-hal/src/rtc_cntl/sleep/esp32c6.rs +++ b/esp-hal/src/rtc_cntl/sleep/esp32c6.rs @@ -568,14 +568,20 @@ impl SleepTimeConfig { pub struct RtcSleepConfig { /// Deep Sleep flag pub deep: bool, - /// Power Down flags - pub pd_flags: PowerDownFlags, - /// Power down the CPU power domain during light sleep. See - /// [`Self::with_cpu_power_down`]. + /// Power Down flags. On the C6 `apply()` is authoritative for the + /// `pd_cpu`/`pd_top` bits, so a domain can't be powered off without the + /// caller's retention storage. + pub(crate) pd_flags: PowerDownFlags, + /// See [`Self::with_cpu_power_down`]. cpu_power_down: bool, - /// Power down the digital TOP power domain during light sleep. See - /// [`Self::with_top_power_down`]. + /// See [`Self::with_top_power_down`]. top_power_down: bool, + /// CPU-domain retention store; null when only clock-gating. A raw pointer + /// (not a borrow) keeps `RtcSleepConfig` `Copy`; the builders take a + /// `&'static mut`, so it stays valid. + cpu_retention_mem: *mut crate::rtc_cntl::cpu_retention::CpuRetentionMemory, + /// TOP-domain system-peripheral regDMA store; null when not opted in. + top_retention_mem: *mut crate::rtc_cntl::retention::SystemRetentionMemory, } impl Default for RtcSleepConfig { @@ -588,6 +594,8 @@ impl Default for RtcSleepConfig { pd_flags: PowerDownFlags(0), cpu_power_down: false, top_power_down: false, + cpu_retention_mem: core::ptr::null_mut(), + top_retention_mem: core::ptr::null_mut(), } } } @@ -595,20 +603,21 @@ impl Default for RtcSleepConfig { impl RtcSleepConfig { /// Power down the CPU power domain during light sleep. /// - /// When enabled, the CPU's register state is saved before sleeping and - /// restored on wakeup entirely in software (the regDMA/PAU engine is not - /// involved), allowing the CPU domain to be powered off for additional - /// savings. This has no effect on deep sleep, where the CPU is always - /// powered down. - /// - /// Use [`cpu_retention::cpu_power_down_wake_count`] to confirm the CPU - /// domain actually lost power across a sleep. + /// The CPU state is saved/restored in software (not regDMA) into the caller's + /// [`CpuRetentionMemory`]. No effect on deep sleep. See + /// [`cpu_retention::cpu_power_down_wake_count`] to confirm the domain lost + /// power. /// + /// [`CpuRetentionMemory`]: crate::rtc_cntl::cpu_retention::CpuRetentionMemory /// [`cpu_retention::cpu_power_down_wake_count`]: crate::rtc_cntl::cpu_retention::cpu_power_down_wake_count #[instability::unstable] #[must_use] - pub fn with_cpu_power_down(mut self, enable: bool) -> Self { - self.cpu_power_down = enable; + pub fn with_cpu_power_down( + mut self, + memory: &'static mut crate::rtc_cntl::cpu_retention::CpuRetentionMemory, + ) -> Self { + self.cpu_power_down = true; + self.cpu_retention_mem = memory; self } @@ -620,22 +629,27 @@ impl RtcSleepConfig { /// Power down the digital `TOP` power domain during light sleep. /// - /// The `TOP` domain holds the core system peripherals (interrupt matrix, - /// HP system, TEE/APM, IO MUX, flash SPI mem, SysTimer, and the PCR clock - /// config). When enabled, their register state is backed up to RAM by the - /// regDMA/PAU engine before sleeping and restored on wakeup, so the domain - /// can be powered off for additional savings. Execution resumes in place - /// (the CPU domain is unaffected unless [`Self::with_cpu_power_down`] is also - /// set). This has no effect on deep sleep. + /// The core system peripherals are backed up to RAM by regDMA into the + /// caller's [`SystemRetentionMemory`] and restored on wakeup; without it the + /// `TOP` domain is only clock-gated. On the C6 this also powers the CPU down, + /// so it also needs a [`CpuRetentionMemory`]. No effect on deep sleep. + /// + /// [`CpuRetentionMemory`]: crate::rtc_cntl::cpu_retention::CpuRetentionMemory + /// [`SystemRetentionMemory`]: crate::rtc_cntl::cpu_retention::SystemRetentionMemory #[instability::unstable] #[must_use] - pub fn with_top_power_down(mut self, enable: bool) -> Self { - self.top_power_down = enable; + pub fn with_top_power_down( + mut self, + cpu_memory: &'static mut crate::rtc_cntl::cpu_retention::CpuRetentionMemory, + system_memory: &'static mut crate::rtc_cntl::retention::SystemRetentionMemory, + ) -> Self { + self.top_power_down = true; + self.cpu_retention_mem = cpu_memory; + self.top_retention_mem = system_memory; self } - /// Returns whether the digital TOP power domain is powered down during light - /// sleep. + /// Returns whether the TOP power domain is powered down during light sleep. #[instability::unstable] pub fn top_power_down(&self) -> bool { self.top_power_down @@ -792,24 +806,25 @@ impl RtcSleepConfig { self.pd_flags.set_pd_rc_fast(true); self.pd_flags.set_pd_xtal32k(!lp_slow_uses_xtal32k); - // Optionally power down the CPU domain. Its register state is saved - // and restored in software around the sleep (see `cpu_retention`). - if self.cpu_power_down { - self.pd_flags.set_pd_cpu(true); - } - - // Optionally power down the digital TOP domain. Its peripheral - // register state is backed up/restored by regDMA around the sleep - // (see `retention`). On C6 the CPU cannot survive a TOP power-down - // (its execution context is torn down and the ROM restarts on - // wakeup), so TOP power-down implies CPU power-down: IDF routes - // both through the software CPU-retention wake-stub path (see - // esp-idf sleep_modes.c `esp_sleep_cpu_retention` gated on - // PMU_SLEEP_PD_CPU | PMU_SLEEP_PD_TOP). - if self.top_power_down { - self.pd_flags.set_pd_top(true); - self.pd_flags.set_pd_cpu(true); - } + // Only power a domain down when the caller gave us the storage to + // restore it and no active peripheral holds a power-domain lock on + // it; otherwise fall back to clock-gating. + use crate::rtc_cntl::power_domain::{Domain, can_power_down}; + let have_cpu_mem = !self.cpu_retention_mem.is_null(); + let have_sys_mem = !self.top_retention_mem.is_null(); + + // TOP-pd needs both the CPU frame buffer (TOP-pd implies CPU-pd on + // the C6) and the system-peripheral regDMA buffer. + let top_pd = self.top_power_down + && have_cpu_mem + && have_sys_mem + && can_power_down(Domain::Top); + let cpu_pd = self.cpu_power_down && have_cpu_mem && can_power_down(Domain::Cpu); + + // On C6 the CPU cannot survive a TOP power-down, so pd_top implies + // pd_cpu (both go through the software CPU-retention wake stub). + self.pd_flags.set_pd_top(top_pd); + self.pd_flags.set_pd_cpu(cpu_pd || top_pd); } } @@ -899,20 +914,21 @@ impl RtcSleepConfig { // Start entry into sleep mode - // Arm regDMA retention of the TOP-domain peripherals so the PMU backs - // them up on sleep entry and restores them on wakeup. Must happen after - // the power config write above (which resets the PMU backup-enable bits) - // and before the sleep request. - if !self.deep && self.pd_flags.pd_top() { - crate::rtc_cntl::retention::enable_top_retention(); + // Arm regDMA retention of the TOP-domain peripherals. Must happen after + // the power config write above (which resets the backup-enable bits) and + // before the sleep request. + if !self.deep && self.pd_flags.pd_top() && !self.top_retention_mem.is_null() { + let system_memory = unsafe { &mut *self.top_retention_mem }; + crate::rtc_cntl::retention::enable_top_retention(system_memory); } - if !self.deep && self.pd_flags.pd_cpu() { - // CPU power-down light sleep: save the CPU register state, trigger - // sleep and (on the same call) resume here after wakeup with the - // state restored. The sleep trigger + wait loop happen inside. + if !self.deep && self.pd_flags.pd_cpu() && !self.cpu_retention_mem.is_null() { + // CPU power-down light sleep: save state, sleep, and resume here + // with it restored. The pointer is non-null whenever apply() sets + // pd_cpu, as that only happens via the buffer-carrying builders. + let memory = unsafe { &mut *self.cpu_retention_mem }; unsafe { - crate::rtc_cntl::cpu_retention::sleep_with_cpu_retention(); + crate::rtc_cntl::cpu_retention::sleep_with_cpu_retention(memory); } } else { // pmu_ll_hp_set_sleep_enable @@ -929,12 +945,9 @@ impl RtcSleepConfig { } } - // After a TOP-domain power-down light sleep, TIMG0's flashboot watchdog - // comes back armed: TIMG0 sits in the TOP domain, is reset when TOP - // powers up, and is deliberately not part of the regDMA retention list. - // IDF disables it in software on wakeup (misc_modules_wake_prepare(), - // sleep_modes.c). Do the same here or it will reset the chip shortly - // after we resume. + // After a TOP power-down, TIMG0 (in the TOP domain, not retained) comes + // back with its flashboot watchdog armed. Disable it, like IDF's + // misc_modules_wake_prepare(), or it resets the chip shortly after. if !self.deep && self.pd_flags.pd_top() { let tg0 = crate::peripherals::TIMG0::regs(); tg0.wdtwprotect().write(|w| unsafe { w.bits(0x50D8_3AA1) }); diff --git a/esp-hal/src/spi/master/low_level/mod.rs b/esp-hal/src/spi/master/low_level/mod.rs index 5cbc011f9a7..b1cbb03a140 100644 --- a/esp-hal/src/spi/master/low_level/mod.rs +++ b/esp-hal/src/spi/master/low_level/mod.rs @@ -47,6 +47,12 @@ mod version; pub(super) struct SpiWrapper<'d> { pub(super) spi: AnySpi<'d>, _guard: PeripheralGuard, + // Active keeps `TOP` powered; `Spi::with_retention_memory` swaps to retained. + #[cfg(esp32c6)] + pub(super) power: crate::rtc_cntl::retention::PowerManagement< + 'd, + crate::rtc_cntl::retention::SpiRetentionMemory, + >, } impl<'d> SpiWrapper<'d> { @@ -55,6 +61,8 @@ impl<'d> SpiWrapper<'d> { let this = Self { spi: spi.degrade(), _guard: PeripheralGuard::new(p), + #[cfg(esp32c6)] + power: crate::rtc_cntl::retention::PowerManagement::new(), }; // Initialize state diff --git a/esp-hal/src/spi/master/mod.rs b/esp-hal/src/spi/master/mod.rs index e742728e933..4c0f28c07db 100644 --- a/esp-hal/src/spi/master/mod.rs +++ b/esp-hal/src/spi/master/mod.rs @@ -51,6 +51,9 @@ pub use low_level::{Info, Instance, QspiInstance, State}; use procmacros::doc_replace; use super::{BitOrder, Error, Mode}; +#[cfg(esp32c6)] +#[instability::unstable] +pub use crate::rtc_cntl::retention::SpiRetentionMemory; use crate::{ Async, Blocking, @@ -992,6 +995,17 @@ impl<'d, Dm> Spi<'d, Dm> where Dm: DriverMode, { + /// Retain this SPI's config registers in `mem` across a `TOP` power-down in + /// light sleep. While active the driver keeps `TOP` powered; this drops that + /// lock and lets regDMA save/restore the config so `TOP` can power down. + #[cfg(esp32c6)] + #[instability::unstable] + pub fn with_retention_memory(mut self, mem: &'d mut SpiRetentionMemory) -> Self { + let base = self.driver().regs() as *const _ as usize as u32; + self.spi.power.retain(mem, base); + self + } + fn connect_sio_pin(&self, pin: interconnect::OutputSignal<'d>, n: usize) -> PinGuard { let in_signal = self.spi.info().sio_input(n); let out_signal = self.spi.info().sio_output(n); diff --git a/esp-hal/src/uart/mod.rs b/esp-hal/src/uart/mod.rs index 953bca9de50..77c7f01cdd6 100644 --- a/esp-hal/src/uart/mod.rs +++ b/esp-hal/src/uart/mod.rs @@ -75,6 +75,11 @@ use low_level::{ sync_regs, }; +#[cfg(not(esp32c6))] +use crate::rtc_cntl::WakeLock; +#[cfg(esp32c6)] +#[instability::unstable] +pub use crate::rtc_cntl::retention::UartRetentionMemory; use crate::{ Async, Blocking, @@ -551,6 +556,9 @@ where guard: rx_guard, peri_clock_guard: peri_clock_guard.clone(), // Receiving data continuously, the peripheral can't let the system sleep. + #[cfg(esp32c6)] + power: crate::rtc_cntl::retention::PowerManagement::new(), + #[cfg(not(esp32c6))] _wake_lock: WakeLock::new(), reported_errors: config.rx.reported_errors, }, @@ -609,7 +617,11 @@ pub struct UartRx<'d, Dm: DriverMode> { phantom: PhantomData, guard: PeripheralGuard, peri_clock_guard: UartClockGuard<'d>, + // Active keeps `TOP` powered; `Uart::with_retention_memory` swaps to retained. + #[cfg(esp32c6)] + power: crate::rtc_cntl::retention::PowerManagement<'d, UartRetentionMemory>, // Receiving data continuously, the peripheral can't let the system sleep. + #[cfg(not(esp32c6))] _wake_lock: WakeLock, reported_errors: EnumSet, } @@ -1075,6 +1087,9 @@ impl<'d> UartRx<'d, Blocking> { phantom: PhantomData, guard: self.guard, peri_clock_guard: self.peri_clock_guard, + #[cfg(esp32c6)] + power: self.power, + #[cfg(not(esp32c6))] _wake_lock: self._wake_lock, reported_errors: self.reported_errors, } @@ -1098,6 +1113,9 @@ impl<'d> UartRx<'d, Async> { phantom: PhantomData, guard: self.guard, peri_clock_guard: self.peri_clock_guard, + #[cfg(esp32c6)] + power: self.power, + #[cfg(not(esp32c6))] _wake_lock: self._wake_lock, reported_errors: self.reported_errors, } @@ -1849,6 +1867,18 @@ where self.tx.uart.info().regs() } + /// Retain this UART's config registers in `mem` across a `TOP` power-down in + /// light sleep. While active the driver keeps `TOP` powered; this drops that + /// lock and lets regDMA save/restore the config so `TOP` can power down. The + /// console/log UART is retained automatically and does not need this. + #[cfg(esp32c6)] + #[instability::unstable] + pub fn with_retention_memory(mut self, mem: &'d mut UartRetentionMemory) -> Self { + let base = self.regs() as *const RegisterBlock as usize as u32; + self.rx.power.retain(mem, base); + self + } + #[procmacros::doc_replace] /// Returns whether the UART TX buffer is ready to accept more data. /// diff --git a/qa-test/src/bin/sleep_timer_powerdown.rs b/qa-test/src/bin/sleep_timer_powerdown.rs index d0c6f8a5249..38bd9f46ff4 100644 --- a/qa-test/src/bin/sleep_timer_powerdown.rs +++ b/qa-test/src/bin/sleep_timer_powerdown.rs @@ -1,52 +1,24 @@ -//! Timer-woken light sleep with the CPU/TOP power domains powered down, with a -//! built-in software proof that power was actually removed. +//! Timer-woken light sleep with the CPU/TOP power domains powered down. //! -//! The example walks three sleep policies for the same timer wakeup: +//! Three policies for the same timer wakeup: clock-gated (plain light sleep), +//! cpu-powerdown ([`with_cpu_power_down`], CPU state saved in software) and +//! top-powerdown ([`with_top_power_down`], whole `TOP` domain off via regDMA; +//! also powers the CPU down on the C6). Each round prints the time slept and +//! [`cpu_power_down_wake_count`], which only advances when the CPU lost power. //! -//! - **clock-gated** ([`RtcSleepConfig::default`]): a plain light sleep. The CPU is only -//! clock-gated - execution resumes in place and no register state is lost. -//! - **cpu-powerdown** ([`with_cpu_power_down`]): the CPU domain is powered off during sleep. Its -//! state is saved/restored in software (the ROM wake stub, see `cpu_retention`), so execution -//! still resumes in place. -//! - **top-powerdown** ([`with_top_power_down`]): the whole digital `TOP` domain is powered off. -//! The core system peripherals (interrupt matrix, HP system, TEE/APM, IO MUX, flash SPI mem, -//! SysTimer, PCR clocks, console UART) lose their state, so the regDMA/PAU engine backs them up -//! to RAM before sleep and the PMU restores them on wakeup. On the C6 this also powers down the -//! CPU domain. +//! It also proves the design end to end: //! -//! ## Software proof (no instruments) +//! - **Negative control**: an un-retained `TOP` register (I2C0 `SCL_LOW_PERIOD`) survives a +//! clock-gated sleep; once its driver is dropped (releasing the power-domain lock that would +//! otherwise block the power-down) a `top-powerdown` wipes it and the CPU-power-down counter +//! advances - direct evidence the domain lost power. +//! - **Safety net**: while UART1 is active and un-retained it holds a `TOP` power-domain lock, so +//! `top-powerdown` degrades to clock-gating (the counter stays put). +//! - **Opt-in retention**: UART1/I2C0/SPI2 are retained, then a config register of each is +//! confirmed to survive `top-powerdown`. //! -//! [`cpu_power_down_wake_count`] is incremented from *inside* the ROM wake-stub -//! restore path, so it advances **only** when the CPU domain genuinely lost -//! power. For each round the example prints the wall-clock time actually spent -//! asleep (measured with the always-on RTC, which no sleep can stop) and that -//! counter: -//! -//! ```text -//! clock-gated #1: slept ~1000 ms (RTC), CPU power-downs = 0 -//! cpu-powerdown #1: slept ~1000 ms (RTC), CPU power-downs = 1 -//! top-powerdown #1: slept ~1000 ms (RTC), CPU power-downs = 4 -//! ``` -//! -//! The counter stays `0` for the clock-gated rounds and increments for every -//! power-down round - that is the definitive proof the CPU domain lost power and -//! resumed through the wake stub. Every mode sleeps for the full duration (the -//! RTC confirms the chip idled, it did not busy-wait), and there is no ROM -//! reboot banner between rounds, which shows peripheral state survived. -//! -//! The *system timer* ([`esp_hal::time::Instant`]) keeps counting through light -//! sleep on the C6 (it stays clocked for timekeeping), so it is deliberately not -//! used as the proof here. -//! -//! ## Measuring current (e.g. Nordic PPK2) -//! -//! GPIO5 is driven high while awake and low while asleep, so it brackets each -//! sleep window on a PPK2/logic-analyzer digital channel. Power the module's -//! 3V3 rail from the PPK2 in source-meter mode (USB unplugged), wire GPIO5 to a -//! logic input, and average the current over a sleep plateau. Because the three -//! modes run back-to-back you get their sleep floors in one capture: each -//! deeper power-down should show a lower floor, with small current shoulders at -//! the window edges from the regDMA / CPU-context save & restore. +//! GPIO5 is high while awake, low while asleep, to bracket each sleep for a +//! current meter / logic analyzer. //% CHIP_FILTER: esp32c6 @@ -57,33 +29,41 @@ use esp_backtrace as _; use esp_hal::{ delay::Delay, gpio::{Level, Output, OutputConfig}, + i2c::master::{Config as I2cConfig, I2c, I2cRetentionMemory}, main, rtc_cntl::{ Rtc, - cpu_retention::cpu_power_down_wake_count, - sleep::{RtcSleepConfig, TimerWakeupSource}, + cpu_retention::{CpuRetentionMemory, SystemRetentionMemory, cpu_power_down_wake_count}, + sleep::{LowPower, RtcSleepConfig, TimerWakeupSource}, }, + spi::master::{Config as SpiConfig, Spi, SpiRetentionMemory}, time::Duration, + uart::{Config as UartConfig, Uart, UartRetentionMemory}, }; use esp_println::println; esp_bootloader_esp_idf::esp_app_desc!(); -/// Sleep duration per round, in milliseconds. Long enough to give a current -/// meter a wide, flat sleep plateau to average over. +/// Allocate `$val` in a `static` and hand out a `&'static mut` to it. +macro_rules! mk_static { + ($t:ty, $val:expr) => {{ + static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); + STATIC_CELL.uninit().write($val) + }}; +} + +/// Sleep duration per round (ms), wide enough to average on a current meter. const EVENT_MS: u64 = 1000; -/// Awake window between sleeps, in milliseconds, so the sleep plateaus are -/// clearly separated on a current/logic trace. +/// Awake window between sleeps (ms), to separate the plateaus on a trace. const AWAKE_MS: u32 = 200; /// Rounds per mode. const ROUNDS: u32 = 3; -/// Sleep once for `EVENT_MS`, then report the wall-clock time spent asleep (from -/// the always-on RTC) and the CPU power-down wake counter. `marker` is driven -/// low for the sleep window (high while awake) so a PPK2/logic channel can -/// bracket each sleep. +/// Sleep once for `EVENT_MS` and report the time slept (from the RTC) and the +/// CPU power-down count. `marker` is driven low for the sleep window. fn sleep_round( rtc: &mut Rtc<'_>, + lpwr: &mut LowPower<'_>, marker: &mut Output<'_>, delay: &Delay, config: &RtcSleepConfig, @@ -94,7 +74,7 @@ fn sleep_round( let rtc_before = rtc.time_since_power_up().as_micros(); marker.set_low(); - rtc.sleep(config, &[&timer]); + lpwr.sleep(config, &[&timer]); marker.set_high(); let slept_ms = (rtc.time_since_power_up().as_micros() - rtc_before) / 1000; @@ -112,40 +92,211 @@ fn sleep_round( #[main] fn main() -> ! { let peripherals = esp_hal::init(esp_hal::Config::default()); - let mut rtc = Rtc::new(peripherals.LPWR); + let mut rtc = Rtc::new(peripherals.RTC_TIMER); + let mut lpwr = LowPower::new(peripherals.LPWR); let delay = Delay::new(); - // Awake = high, asleep = low. The IO domain stays powered through light - // sleep, so the pin holds its level and a meter/scope sees a clean window. + // Awake = high, asleep = low. The IO domain stays powered, so the pin holds. let mut marker = Output::new(peripherals.GPIO5, Level::High, OutputConfig::default()); + // Each power-down config gets its own caller-owned retention buffer. + let clock_gated = RtcSleepConfig::default(); + let cpu_pd = RtcSleepConfig::default() + .with_cpu_power_down(mk_static!(CpuRetentionMemory, CpuRetentionMemory::new())); + let top_pd = RtcSleepConfig::default().with_top_power_down( + mk_static!(CpuRetentionMemory, CpuRetentionMemory::new()), + mk_static!(SystemRetentionMemory, SystemRetentionMemory::new()), + ); + + println!("up and running!"); + + // Negative control: I2C0's SCL_LOW_PERIOD (a TOP register) survives a + // clock-gated sleep, but once its driver is dropped a top-powerdown wipes + // it. A live driver holds a TOP power-domain lock (see the safety net + // below), so it has to be released first for the domain to actually power + // down - which is exactly what proves the domain lost power. + const I2C0_SCL_LOW_PERIOD: *const u32 = 0x6000_4000 as *const u32; + let i2c0 = I2c::new(peripherals.I2C0, I2cConfig::default()).unwrap(); + // SAFETY: driver alive, register readable. + let scl_configured = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; + + marker.set_low(); + lpwr.sleep( + &clock_gated, + &[&TimerWakeupSource::new(Duration::from_millis(EVENT_MS))], + ); + marker.set_high(); + // Read while the driver is still alive (TOP stayed powered, so it survives). + let scl_after_clock_gate = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; + + // Drop the driver so its TOP power-domain lock is released and the domain can + // actually power down (nothing else holds one yet). + core::mem::drop(i2c0); + + let downs_before_nc = cpu_power_down_wake_count(); + marker.set_low(); + lpwr.sleep( + &top_pd, + &[&TimerWakeupSource::new(Duration::from_millis(EVENT_MS))], + ); + marker.set_high(); + // SAFETY: register readable; reads 0 now that TOP lost power (and reset it). + let scl_after_top_pd = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; + let downs_after_nc = cpu_power_down_wake_count(); + + println!( + "negative-control I2C0 SCL_LOW_PERIOD: configured = {:#x}", + scl_configured + ); + println!( + " after clock-gated (== no-retention/main behavior): {:#x} -> {}", + scl_after_clock_gate, + if scl_after_clock_gate == scl_configured { + "SURVIVED (TOP stayed powered)" + } else { + "CHANGED?!" + } + ); + println!( + " after top-powerdown (no retention): {:#x} -> {} (CPU power-downs {} -> {})", + scl_after_top_pd, + if scl_after_top_pd != scl_configured && scl_after_top_pd == 0 { + "WIPED (TOP lost power) -> POWER REALLY REMOVED" + } else { + "STILL SET?!" + }, + downs_before_nc, + downs_after_nc + ); + + // UART1 is live but not yet retained, so it holds a TOP power-domain lock: + // top-powerdown must degrade to clock-gating (counter must not advance). + const UART1_CLKDIV: *const u32 = 0x6000_1014 as *const u32; + let uart1 = Uart::new(peripherals.UART1, UartConfig::default()).unwrap(); + + let downs_before_block = cpu_power_down_wake_count(); + for round in 1..=2 { + sleep_round( + &mut rtc, + &mut lpwr, + &mut marker, + &delay, + &top_pd, + "top-blocked ", + round, + ); + } + let downs_after_block = cpu_power_down_wake_count(); + println!( + "safety-net: uart1 active + un-retained -> top-powerdown degraded, CPU power-downs {} (was {}) -> {}", + downs_after_block, + downs_before_block, + if downs_after_block == downs_before_block { + "BLOCKED (clock-gated) -> SAFE" + } else { + "POWERED DOWN -> UNSAFE" + } + ); + + // Retain UART1: drops the lock and stores the memory in the driver, so its + // registers are saved/restored around the power-down. Kept alive for the proof. + let _uart1 = + uart1.with_retention_memory(mk_static!(UartRetentionMemory, UartRetentionMemory::new())); + // SAFETY: driver alive, register readable. + let clkdiv_before = unsafe { UART1_CLKDIV.read_volatile() }; + + // Re-acquire I2C0 (the negative control dropped it) and retain it too. + // SAFETY: the earlier I2C0 driver was dropped, so no other instance is live. + let i2c0 = I2c::new( + unsafe { esp_hal::peripherals::I2C0::steal() }, + I2cConfig::default(), + ) + .unwrap(); + let _i2c0 = + i2c0.with_retention_memory(mk_static!(I2cRetentionMemory, I2cRetentionMemory::new())); + // SAFETY: driver alive, register readable. + let i2c_scl_before = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; + + // ...and SPI2, whose CLOCK register holds the divider from `Spi::new`. + const SPI2_CLOCK: *const u32 = 0x6008_100C as *const u32; + let spi2 = Spi::new(peripherals.SPI2, SpiConfig::default()).unwrap(); + let _spi2 = + spi2.with_retention_memory(mk_static!(SpiRetentionMemory, SpiRetentionMemory::new())); + // SAFETY: driver alive, register readable. + let spi_clock_before = unsafe { SPI2_CLOCK.read_volatile() }; + // Same timer wakeup, increasingly aggressive power policies. let modes: [(&str, RtcSleepConfig); 3] = [ - ("clock-gated ", RtcSleepConfig::default()), - ( - "cpu-powerdown", - RtcSleepConfig::default().with_cpu_power_down(true), - ), - ( - "top-powerdown", - RtcSleepConfig::default().with_top_power_down(true), - ), + ("clock-gated ", clock_gated), + ("cpu-powerdown", cpu_pd), + ("top-powerdown", top_pd), ]; - println!("up and running!"); - for (label, config) in &modes { for round in 1..=ROUNDS { - sleep_round(&mut rtc, &mut marker, &delay, config, label, round); + sleep_round( + &mut rtc, + &mut lpwr, + &mut marker, + &delay, + config, + label, + round, + ); } } - // Keep going in the deepest mode so the counter can be watched climbing and - // a meter has a steady stream of identical sleep windows to average. - let top = RtcSleepConfig::default().with_top_power_down(true); + // With retention, UART1's CLKDIV survives the power-down. + let clkdiv_after = unsafe { UART1_CLKDIV.read_volatile() }; + println!( + "UART1 CLKDIV: before = {:#x}, after top-powerdown = {:#x} -> {}", + clkdiv_before, + clkdiv_after, + if clkdiv_after == clkdiv_before && clkdiv_after != 0 { + "RETAINED" + } else { + "LOST" + } + ); + + // Same check for I2C0's timing register. + let i2c_scl_after = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; + println!( + "I2C0 SCL_LOW_PERIOD: before = {:#x}, after top-powerdown = {:#x} -> {}", + i2c_scl_before, + i2c_scl_after, + if i2c_scl_after == i2c_scl_before && i2c_scl_after != 0 { + "RETAINED" + } else { + "LOST" + } + ); + + // ...and for SPI2's clock register. + let spi_clock_after = unsafe { SPI2_CLOCK.read_volatile() }; + println!( + "SPI2 CLOCK: before = {:#x}, after top-powerdown = {:#x} -> {}", + spi_clock_before, + spi_clock_after, + if spi_clock_after == spi_clock_before && spi_clock_after != 0 { + "RETAINED" + } else { + "LOST" + } + ); + + // Keep going in the deepest mode for a steady stream of sleep windows. let mut round = ROUNDS; loop { round += 1; - sleep_round(&mut rtc, &mut marker, &delay, &top, "top-powerdown", round); + sleep_round( + &mut rtc, + &mut lpwr, + &mut marker, + &delay, + &top_pd, + "top-powerdown", + round, + ); } } From c332e16385751728b495dab6a8a3b099e94a0114 Mon Sep 17 00:00:00 2001 From: Juraj Sadel Date: Wed, 8 Jul 2026 10:40:05 +0200 Subject: [PATCH 3/8] cleanup --- esp-hal/src/i2c/master/mod.rs | 5 +- esp-hal/src/rtc_cntl/cpu_retention.rs | 151 +++++++++++++---------- esp-hal/src/rtc_cntl/power_domain.rs | 9 +- esp-hal/src/rtc_cntl/retention.rs | 70 ++++++----- esp-hal/src/rtc_cntl/sleep/esp32c6.rs | 52 ++++---- esp-hal/src/spi/master/low_level/mod.rs | 12 +- esp-hal/src/spi/master/mod.rs | 9 +- esp-hal/src/uart/mod.rs | 7 +- qa-test/src/bin/sleep_timer_powerdown.rs | 69 +++++++---- 9 files changed, 213 insertions(+), 171 deletions(-) diff --git a/esp-hal/src/i2c/master/mod.rs b/esp-hal/src/i2c/master/mod.rs index 4a1d87e4575..836d3ec1534 100644 --- a/esp-hal/src/i2c/master/mod.rs +++ b/esp-hal/src/i2c/master/mod.rs @@ -684,7 +684,7 @@ pub struct I2c<'d, Dm: DriverMode> { phantom: PhantomData, guard: PeripheralGuard, config: DriverConfig, - // Active keeps `TOP` powered; `I2c::with_retention_memory` swaps to retained. + // Active keeps `TOP` powered; `with_retention_memory` swaps to retained. #[cfg(esp32c6)] power: crate::rtc_cntl::retention::PowerManagement< 'd, @@ -1055,8 +1055,7 @@ where } /// Retain this I2C's config registers in `mem` across a `TOP` power-down in - /// light sleep. While active the driver keeps `TOP` powered; this drops that - /// lock and lets regDMA save/restore the config so `TOP` can power down. + /// light sleep, dropping the lock that would otherwise keep `TOP` powered. #[cfg(esp32c6)] #[instability::unstable] pub fn with_retention_memory(mut self, mem: &'d mut I2cRetentionMemory) -> Self { diff --git a/esp-hal/src/rtc_cntl/cpu_retention.rs b/esp-hal/src/rtc_cntl/cpu_retention.rs index b61571f997b..42599082aff 100644 --- a/esp-hal/src/rtc_cntl/cpu_retention.rs +++ b/esp-hal/src/rtc_cntl/cpu_retention.rs @@ -1,50 +1,40 @@ //! CPU power-down retention during light sleep (ESP32-C6). //! -//! During light sleep the C6 can power down the CPU domain (`pd_cpu`) while the -//! rest of the digital system stays powered, losing all CPU state. The register -//! file and CSRs aren't reachable by regDMA, so (like ESP-IDF's -//! `esp_sleep_cpu_retention()`) they are saved/restored in software. The ~1 KiB -//! backing RAM ([`CpuRetentionMemory`]) is caller-owned and opt-in via -//! [`RtcSleepConfig::with_cpu_power_down`]; without it the CPU is only -//! clock-gated. +//! When the C6 powers down `pd_cpu` in light sleep the CPU loses all state. +//! regDMA can't reach the register file/CSRs, so they are saved/restored in +//! software. Backing RAM +//! ([`CpuRetentionMemory`]) is caller-owned, opt-in via +//! [`RtcSleepConfig::with_cpu_power_down`]; without it the CPU is clock-gated. //! //! [`RtcSleepConfig::with_cpu_power_down`]: crate::rtc_cntl::sleep::RtcSleepConfig::with_cpu_power_down //! -//! Save/restore is in three parts, matching ESP-IDF: +//! Save/restore has three parts: critical registers (GP + machine CSRs) in +//! assembly via a setjmp/longjmp-style trick; non-critical CSRs via `csrr`/ +//! `csrw`; and CPU-domain device registers (`INTPRI`, `PLIC`/`CLINT`, cache). //! -//! 1. **Critical registers** (GP registers + `mepc`/`mstatus`/`mtvec`/...): -//! saved/restored in assembly via a `setjmp`/`longjmp`-style trick - on -//! wakeup the ROM jumps to the restore routine, which returns as if save had. -//! 2. **Non-critical CSRs** (PMP/PMA, trigger module, perf counters, ...): via -//! `csrr`/`csrw`. -//! 3. **CPU-domain device registers** (`INTPRI`, `PLIC`/`CLINT`, L1 cache). -//! -//! The whole path runs from IRAM (`.rwtext`): the ROM jumps to the wake stub in -//! `LP_AON_STORE8` with the flash cache lost, so every function here is `#[ram]` -//! and must not call flash-resident code until the cache config is restored. -//! -//! References (ESP-IDF `v5.4`, ESP32-C6): `esp_hw_support/.../esp32c6/` -//! `sleep_cpu.c`, `sleep_cpu_asm.S`, `include/rvsleep-frames.h`. +//! Everything runs from IRAM (`.rwtext`): the ROM jumps to the wake stub with +//! the flash cache lost, so every function is `#[ram]` and must not call +//! flash-resident code until the cache config is restored. + +// Software register retention here mirrors ESP-IDF's `esp_sleep_cpu_retention()`. +// Ported from ESP-IDF `v5.4` `esp32c6` `sleep_cpu.c`, `sleep_cpu_asm.S`, +// `rvsleep-frames.h`. use core::sync::atomic::{AtomicU32, Ordering}; use procmacros::ram; use crate::peripherals::{LP_AON, PMU}; - -/// Caller-owned backing store for TOP-domain system-peripheral retention. -/// -/// Re-exported here because it is the second buffer required by +/// Second buffer required by /// [`RtcSleepConfig::with_top_power_down`](crate::rtc_cntl::sleep::RtcSleepConfig::with_top_power_down). #[instability::unstable] pub use crate::rtc_cntl::retention::SystemRetentionMemory; -/// Bumped from the ROM wake stub, i.e. only when the CPU domain actually lost -/// and regained power (not on a rejected or clock-gated sleep). +/// Incremented only when the CPU domain actually lost and regained power. static CPU_POWERDOWN_WAKES: AtomicU32 = AtomicU32::new(0); /// How many times the CPU power domain was actually powered down and restored. -/// A diagnostic: if it rises across light sleeps, the CPU genuinely lost power. +/// Diagnostic: rises across light sleeps only if the CPU genuinely lost power. #[instability::unstable] pub fn cpu_power_down_wake_count() -> u32 { CPU_POWERDOWN_WAKES.load(Ordering::Relaxed) @@ -54,8 +44,8 @@ pub fn cpu_power_down_wake_count() -> u32 { // Critical register frame (RvCoreCriticalSleepFrame) // --------------------------------------------------------------------------- -// A raw word buffer addressed by byte offset (`RV_SLP_CTX_*`) from the assembly -// below; layout matches ESP-IDF's `rvsleep-frames.h`: +// Word buffer addressed by byte offset (`RV_SLP_CTX_*`) from the assembly below; +// layout matches ESP-IDF's `rvsleep-frames.h`: // // 0: mepc 1: ra 2: sp 3: gp 4: tp // 5: t0 6: t1 7: t2 8: s0 9: s1 @@ -63,19 +53,19 @@ pub fn cpu_power_down_wake_count() -> u32 { // 32: mstatus 33: mtvec 34: mcause 35: mtval 36: mie 37: mip 38: pmufunc const CRITICAL_FRAME_WORDS: usize = 39; -/// `pmufunc` slot. `pmufunc & 0x3`: `1` = going to sleep, `3` = resumed via the -/// wake stub. +// `pmufunc` slot. `pmufunc & 0x3`: `1` = going to sleep, `3` = resumed via the +// wake stub. const PMUFUNC_WORD: usize = 38; /// Pointer the assembly reads to find the critical frame. Set before every sleep. static mut RV_CORE_CRITICAL_REGS_FRAME: *mut u32 = core::ptr::null_mut(); unsafe extern "C" { - /// Save the CPU critical registers into `RV_CORE_CRITICAL_REGS_FRAME`, mark - /// the frame "going to sleep", and return the frame pointer. + /// Save the critical registers into `RV_CORE_CRITICAL_REGS_FRAME`, mark the + /// frame "going to sleep", and return the frame pointer. fn rv_core_critical_regs_save() -> *mut u32; - /// Restore the CPU critical registers. Used as the ROM wake stub: returns as - /// if [`rv_core_critical_regs_save`] had just returned. + /// Restore the critical registers. Used as the ROM wake stub: returns as if + /// [`rv_core_critical_regs_save`] had just returned. fn rv_core_critical_regs_restore() -> *mut u32; } @@ -285,11 +275,10 @@ unsafe fn write_csr(value: u32) { } } -/// Define the non-critical CSRs to retain from one canonical list, generating -/// the slot count and the save/restore routines so they can't drift. Order -/// mirrors `rv_core_noncritical_regs_{save,restore}()` in ESP-IDF `sleep_cpu.c`. -/// `$name` is documentation only; the CSR is addressed by number so the custom -/// Espressif CSRs work without assembler support. +/// Generate the slot count and save/restore routines for the non-critical CSRs +/// from one list. `$name` is documentation only; CSRs are addressed by number +/// so custom Espressif CSRs need no assembler support. +// CSR list order matches ESP-IDF `sleep_cpu.c`. macro_rules! noncritical_csrs { ($($name:ident = $csr:literal),+ $(,)?) => { /// Non-critical CSR slot count; sizes the `noncritical` field. @@ -378,37 +367,67 @@ const fn total_words(regions: &[Region]) -> usize { // Interrupt matrix priority registers (`INTPRI`, base 0x600C_5000). const INTPRI_REGIONS: [Region; 2] = [ // INTPRI_CORE0_CPU_INT_ENABLE_REG ..= INTPRI_RND_ECO_LOW_REG - Region { start: 0x600C_5000, words: 45 }, + Region { + start: 0x600C_5000, + words: 45, + }, // INTPRI_RND_ECO_HIGH_REG - Region { start: 0x600C_53FC, words: 1 }, + Region { + start: 0x600C_53FC, + words: 1, + }, ]; // L1 cache control (`EXTMEM`, base 0x600C_8000). const CACHE_REGIONS: [Region; 2] = [ // EXTMEM_L1_CACHE_CTRL_REG - Region { start: 0x600C_8004, words: 1 }, + Region { + start: 0x600C_8004, + words: 1, + }, // EXTMEM_L1_CACHE_WRAP_AROUND_CTRL_REG - Region { start: 0x600C_8020, words: 1 }, + Region { + start: 0x600C_8020, + words: 1, + }, ]; // PLIC machine/user interrupt controllers (bases 0x2000_1000 / 0x2000_1400). const PLIC_REGIONS: [Region; 4] = [ // PLIC_MXINT_ENABLE_REG ..= PLIC_MXINT_CLAIM_REG - Region { start: 0x2000_1000, words: 38 }, + Region { + start: 0x2000_1000, + words: 38, + }, // PLIC_MXINT_CONF_REG - Region { start: 0x2000_13FC, words: 1 }, + Region { + start: 0x2000_13FC, + words: 1, + }, // PLIC_UXINT_ENABLE_REG ..= PLIC_UXINT_CLAIM_REG - Region { start: 0x2000_1400, words: 38 }, + Region { + start: 0x2000_1400, + words: 38, + }, // PLIC_UXINT_CONF_REG - Region { start: 0x2000_17FC, words: 1 }, + Region { + start: 0x2000_17FC, + words: 1, + }, ]; // CLINT machine/user timers (bases 0x2000_1800 / 0x2000_1C00). const CLINT_REGIONS: [Region; 2] = [ // CLINT_MINT_SIP_REG ..= CLINT_MINT_MTIMECMP_H_REG - Region { start: 0x2000_1800, words: 6 }, + Region { + start: 0x2000_1800, + words: 6, + }, // CLINT_UINT_SIP_REG ..= CLINT_UINT_UTIMECMP_H_REG - Region { start: 0x2000_1C00, words: 6 }, + Region { + start: 0x2000_1C00, + words: 6, + }, ]; #[ram] @@ -512,11 +531,11 @@ unsafe fn restore_mstatus(mstatus: u32) { } } -/// Save the CPU critical registers, program the wake stub, request sleep and -/// spin until the PMU reports wakeup or rejection. Mirrors ESP-IDF's -/// `do_cpu_retention()`: the save pass (`pmufunc & 0x3 == 1`) triggers sleep; on -/// wakeup the ROM jumps to the restore routine, which returns here with -/// `pmufunc & 0x3 == 3`. +/// Save critical registers, program the wake stub and request sleep, spinning +/// until wakeup or rejection. The save pass +/// (`pmufunc & 0x3 == 1`) sleeps; on wakeup the ROM jumps to the restore +/// routine, which returns here with `pmufunc & 0x3 == 3`. +// Mirrors ESP-IDF `do_cpu_retention()`. #[ram] fn do_cpu_retention() { let frame = unsafe { rv_core_critical_regs_save() }; @@ -524,12 +543,14 @@ fn do_cpu_retention() { let pmufunc = unsafe { frame.add(PMUFUNC_WORD).read_volatile() }; if pmufunc & 0x3 == 0x1 { // Going to sleep. LP_AON_STORE8 is the ROM wake-stub address register. - LP_AON::regs() - .store8() - .write(|w| unsafe { w.bits(rv_core_critical_regs_restore as *const () as usize as u32) }); + LP_AON::regs().store8().write(|w| unsafe { + w.bits(rv_core_critical_regs_restore as *const () as usize as u32) + }); // pmu_ll_hp_set_sleep_enable - PMU::regs().slp_wakeup_cntl0().write(|w| w.sleep_req().bit(true)); + PMU::regs() + .slp_wakeup_cntl0() + .write(|w| w.sleep_req().bit(true)); // On power-down the CPU loses power here and resumes via the wake stub; // on a rejected sleep we fall out normally. @@ -545,14 +566,14 @@ fn do_cpu_retention() { } } -/// Perform a full CPU-power-down light sleep with software register retention -/// (ESP-IDF's `esp_sleep_cpu_retention()`). Only adds the save/restore around -/// the sleep trigger; the PMU sleep config must already be programmed. +/// CPU-power-down light sleep with software register retention, wrapping the +/// sleep trigger in save/restore. /// /// # Safety /// -/// The PMU must already be configured for a `pd_cpu` light sleep and stopping -/// the CPU must be safe. `mem` must stay valid across the sleep. +/// The PMU must already be configured for a `pd_cpu` light sleep, stopping the +/// CPU must be safe, and `mem` must stay valid across the sleep. +// Mirrors ESP-IDF's `esp_sleep_cpu_retention()`. #[ram] pub(crate) unsafe fn sleep_with_cpu_retention(mem: &mut CpuRetentionMemory) { unsafe { diff --git a/esp-hal/src/rtc_cntl/power_domain.rs b/esp-hal/src/rtc_cntl/power_domain.rs index b48f10728bc..ba1b0f7c3b1 100644 --- a/esp-hal/src/rtc_cntl/power_domain.rs +++ b/esp-hal/src/rtc_cntl/power_domain.rs @@ -1,10 +1,9 @@ //! Power-domain locks for light sleep (ESP32-C6). //! -//! An active, un-retained peripheral in a power-downable domain holds a -//! [`PowerDomainLock`]: unlike a [`WakeLock`](crate::rtc_cntl::WakeLock) it -//! doesn't prevent light sleep, only powering its domain down (which degrades to -//! clock-gating), so it can't lose state. Retaining the peripheral drops the -//! lock and lets regDMA save/restore its state around the power-down instead. +//! An active, un-retained peripheral holds a [`PowerDomainLock`]. Unlike a +//! [`WakeLock`](crate::rtc_cntl::WakeLock) it doesn't prevent sleep, only the +//! power-down of its domain (degrading to clock-gating), so it can't lose state. +//! Retaining it drops the lock and lets regDMA save/restore its state instead. use core::sync::atomic::{AtomicU32, Ordering}; diff --git a/esp-hal/src/rtc_cntl/retention.rs b/esp-hal/src/rtc_cntl/retention.rs index d0a6e72b63b..d825526848f 100644 --- a/esp-hal/src/rtc_cntl/retention.rs +++ b/esp-hal/src/rtc_cntl/retention.rs @@ -1,15 +1,14 @@ //! Register DMA (regDMA) based register retention (ESP32-C6). //! //! The PAU's regDMA engine backs peripheral registers up to RAM and restores -//! them while the `TOP` domain is powered down in light sleep. It walks a linked -//! list of [`RegdmaLink`] nodes over PAU entry link 0 with no CPU involvement. +//! them while the `TOP` domain is powered down in light sleep, walking a linked +//! list of [`RegdmaLink`] nodes on PAU entry link 0 with no CPU involvement. //! `sys_periph` builds the core register set into a caller-owned //! [`SystemRetentionMemory`]; [`enable_top_retention`] chains it with any opt-in -//! peripheral entries before arming the link. Without a `SystemRetentionMemory` -//! the `TOP` domain is only clock-gated. -//! -//! References (ESP-IDF `v5.4`): `soc/regdma.h`, `hal/esp32c6/pau_ll.h`, -//! `hal/esp32c6/pau_hal.c`, `esp_hw_support/port/pau_regdma.c`. +//! peripheral entries before arming the link. Without it `TOP` is clock-gated. + +// References (ESP-IDF `v5.4`): `soc/regdma.h`, `hal/esp32c6/pau_ll.h`, +// `hal/esp32c6/pau_hal.c`, `esp_hw_support/port/pau_regdma.c`. use core::{ marker::PhantomData, @@ -38,9 +37,9 @@ const HEAD_EOF_BIT: u32 = 1 << 31; // end of link enum LinkMode { /// Back up/restore a run of consecutive registers via a RAM buffer. Continuous = 0, - /// Back up/restore a run of registers via a RAM buffer, where a 4-word - /// bitmap selects which registers in the window are actually transferred - /// (skipping e.g. read-only status/FIFO registers interspersed in a block). + /// Like [`Continuous`](Self::Continuous), but a 4-word bitmap selects which + /// registers in the window to transfer (skipping interspersed read-only + /// status/FIFO registers). AddrMap = 1, /// Unconditionally write a masked value to a register. Write = 2, @@ -108,8 +107,7 @@ impl RegdmaLink { } /// An ADDR_MAP node: back up/restore the `count` registers selected by `map` - /// (bit `i` = register at `reg + i * 4`) from `reg` into `storage`, skipping - /// interspersed read-only/FIFO registers. + /// (bit `i` = register at `reg + i * 4`) from `reg` into `storage`. fn addr_map(reg: u32, storage: u32, count: u32, map: [u32; 4]) -> Self { Self { head: Self::head(LinkMode::AddrMap, count, false, false), @@ -224,11 +222,16 @@ const SPI_CMD_OFF: u32 = 0x00; // SPI_CMD_REG: ADDR_MAP window base const SPI_RETENTION_REGS_CNT: u32 = 12; /// `spi_regs_map[4]`: config registers in the `CMD..SLAVE` window. const SPI_REGS_MAP: [u32; 4] = [0x0000_31ff, 0x0100_0000, 0, 0]; -/// A single ADDR_MAP over the config registers. +/// A single ADDR_MAP over the config registers, matching ESP-IDF. const SPI_NODE_COUNT: usize = 1; /// Build the SPI retention sequence for `base` into `nodes`, backing the /// registers up into `storage`. +/// +/// The config registers are only reachable while the SPI function clock runs. +/// `Spi::with_retention_memory` holds that clock for the retention lifetime, so +/// they stay accessible at both backup and restore and this single ADDR_MAP is +/// all that is needed (as in ESP-IDF's `spi2_regs_retention`). fn build_spi_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { nodes[0] = RegdmaLink::addr_map( base + SPI_CMD_OFF, @@ -410,9 +413,9 @@ peripheral_retention_memory!( UART_NODE_COUNT, UART_RETENTION_REGS_CNT as usize, build_uart_seq, - "Caller-owned backing store retaining one UART's config registers across a \ -`TOP` power-down. Passed to \ -[`Uart::with_retention_memory`](crate::uart::Uart::with_retention_memory); the \ + "Caller-owned store retaining one UART's config registers across a `TOP` \ +power-down, passed to \ +[`Uart::with_retention_memory`](crate::uart::Uart::with_retention_memory). The \ console/log UART is retained automatically." ); @@ -421,8 +424,8 @@ peripheral_retention_memory!( I2C_NODE_COUNT, I2C_RETENTION_REGS_CNT as usize, build_i2c_seq, - "Caller-owned backing store retaining one I2C's config registers across a \ -`TOP` power-down. Passed to \ + "Caller-owned store retaining one I2C's config registers across a `TOP` \ +power-down, passed to \ [`I2c::with_retention_memory`](crate::i2c::master::I2c::with_retention_memory). \ See [`UartRetentionMemory`]." ); @@ -432,8 +435,8 @@ peripheral_retention_memory!( SPI_NODE_COUNT, SPI_RETENTION_REGS_CNT as usize, build_spi_seq, - "Caller-owned backing store retaining one SPI's config registers across a \ -`TOP` power-down. Passed to \ + "Caller-owned store retaining one SPI's config registers across a `TOP` \ +power-down, passed to \ [`Spi::with_retention_memory`](crate::spi::master::Spi::with_retention_memory). \ See [`UartRetentionMemory`]." ); @@ -446,11 +449,10 @@ pub(crate) trait RetentionMemory { fn register(&mut self, base: u32) -> NonNull; } -/// A `TOP`-domain peripheral driver's power-management state: either active and -/// holding a [`PowerDomainLock`] (keeping `TOP` powered so it can't lose state, -/// without preventing sleep), or retained (the lock is dropped and regDMA -/// save/restores its config across a `TOP` power-down from `'d`-borrowed memory). -/// Stored in the driver, so the user never juggles a separate guard. +/// A `TOP`-domain driver's power state, stored in the driver: either active, +/// holding a [`PowerDomainLock`] that keeps `TOP` powered (without preventing +/// sleep), or retained, with the lock dropped and regDMA saving/restoring its +/// config across a `TOP` power-down from `'d`-borrowed memory. pub(crate) enum PowerManagement<'d, M: RetentionMemory> { /// Active, not retained: the held lock keeps `TOP` powered. PowerDomainLock { _lock: PowerDomainLock }, @@ -557,8 +559,9 @@ impl SystemRetentionMemory { /// /// Must be called after the PMU power config (which resets the backup-enable /// bits) and before the sleep request. Rebuilds the chain from the live registry -/// every time, so the PAU never walks a deregistered entry. Mirrors ESP-IDF's -/// link setup + `pmu_sleep_enable_regdma_backup()` (active/sleep phases only). +/// every time, so the PAU never walks a deregistered entry. +// Mirrors ESP-IDF's link setup + `pmu_sleep_enable_regdma_backup()` +// (active/sleep phases only). pub(crate) fn enable_top_retention(mem: &mut SystemRetentionMemory) { // pau_ll_enable_bus_clock: enable the regDMA bus clock and release its reset. PCR::regs().regdma_conf().modify(|_, w| { @@ -587,13 +590,12 @@ pub(crate) fn enable_top_retention(mem: &mut SystemRetentionMemory) { .modify(|_, w| w.hp_sleep2active_backup_en().set_bit()); } -/// ESP32-C6 TOP-domain system-peripheral retention link: the register regions -/// for the core system peripherals lost when `TOP` powers down, mirroring -/// ESP-IDF's `SLEEP_RETENTION_MODULE_SYS_PERIPH` + `..._CLOCK_SYSTEM` in -/// retention-priority order (system clock first). -/// -/// References (ESP-IDF `v5.4`): `soc/esp32c6/system_retention_periph.c`, -/// `esp_hw_support/.../sleep_clock.c`, `esp_hw_support/sleep_system_peripheral.c`. +/// TOP-domain system-peripheral retention link: register regions for the core +/// peripherals lost when `TOP` powers down, in retention-priority order (system +/// clock first). +// Mirrors ESP-IDF's `SLEEP_RETENTION_MODULE_SYS_PERIPH` + `..._CLOCK_SYSTEM`. +// References (ESP-IDF `v5.4`): `soc/esp32c6/system_retention_periph.c`, +// `esp_hw_support/.../sleep_clock.c`, `esp_hw_support/sleep_system_peripheral.c`. mod sys_periph { use super::{ HEAD_LENGTH_MASK, diff --git a/esp-hal/src/rtc_cntl/sleep/esp32c6.rs b/esp-hal/src/rtc_cntl/sleep/esp32c6.rs index acb37fcd241..975cbdcfcbc 100644 --- a/esp-hal/src/rtc_cntl/sleep/esp32c6.rs +++ b/esp-hal/src/rtc_cntl/sleep/esp32c6.rs @@ -568,17 +568,15 @@ impl SleepTimeConfig { pub struct RtcSleepConfig { /// Deep Sleep flag pub deep: bool, - /// Power Down flags. On the C6 `apply()` is authoritative for the - /// `pd_cpu`/`pd_top` bits, so a domain can't be powered off without the - /// caller's retention storage. + /// Power Down flags. On the C6 `apply()` sets the `pd_cpu`/`pd_top` bits, so + /// a domain can't power off without the caller's retention storage. pub(crate) pd_flags: PowerDownFlags, /// See [`Self::with_cpu_power_down`]. cpu_power_down: bool, /// See [`Self::with_top_power_down`]. top_power_down: bool, /// CPU-domain retention store; null when only clock-gating. A raw pointer - /// (not a borrow) keeps `RtcSleepConfig` `Copy`; the builders take a - /// `&'static mut`, so it stays valid. + /// (not a borrow) keeps `Self` `Copy`; the builders take a `&'static mut`. cpu_retention_mem: *mut crate::rtc_cntl::cpu_retention::CpuRetentionMemory, /// TOP-domain system-peripheral regDMA store; null when not opted in. top_retention_mem: *mut crate::rtc_cntl::retention::SystemRetentionMemory, @@ -603,7 +601,7 @@ impl Default for RtcSleepConfig { impl RtcSleepConfig { /// Power down the CPU power domain during light sleep. /// - /// The CPU state is saved/restored in software (not regDMA) into the caller's + /// CPU state is saved/restored in software into the caller's /// [`CpuRetentionMemory`]. No effect on deep sleep. See /// [`cpu_retention::cpu_power_down_wake_count`] to confirm the domain lost /// power. @@ -629,10 +627,10 @@ impl RtcSleepConfig { /// Power down the digital `TOP` power domain during light sleep. /// - /// The core system peripherals are backed up to RAM by regDMA into the - /// caller's [`SystemRetentionMemory`] and restored on wakeup; without it the - /// `TOP` domain is only clock-gated. On the C6 this also powers the CPU down, - /// so it also needs a [`CpuRetentionMemory`]. No effect on deep sleep. + /// Core system peripherals are backed up to the caller's + /// [`SystemRetentionMemory`] by regDMA and restored on wakeup. This also + /// powers the CPU down, so it needs a [`CpuRetentionMemory`] too. No effect + /// on deep sleep. /// /// [`CpuRetentionMemory`]: crate::rtc_cntl::cpu_retention::CpuRetentionMemory /// [`SystemRetentionMemory`]: crate::rtc_cntl::cpu_retention::SystemRetentionMemory @@ -806,23 +804,19 @@ impl RtcSleepConfig { self.pd_flags.set_pd_rc_fast(true); self.pd_flags.set_pd_xtal32k(!lp_slow_uses_xtal32k); - // Only power a domain down when the caller gave us the storage to - // restore it and no active peripheral holds a power-domain lock on - // it; otherwise fall back to clock-gating. + // Only power a domain down with the caller's retention storage and no + // active power-domain lock on it; otherwise fall back to clock-gating. use crate::rtc_cntl::power_domain::{Domain, can_power_down}; let have_cpu_mem = !self.cpu_retention_mem.is_null(); let have_sys_mem = !self.top_retention_mem.is_null(); - // TOP-pd needs both the CPU frame buffer (TOP-pd implies CPU-pd on - // the C6) and the system-peripheral regDMA buffer. - let top_pd = self.top_power_down - && have_cpu_mem - && have_sys_mem - && can_power_down(Domain::Top); + // TOP-pd needs both the CPU frame buffer (it implies CPU-pd) and the + // system-peripheral regDMA buffer. + let top_pd = + self.top_power_down && have_cpu_mem && have_sys_mem && can_power_down(Domain::Top); let cpu_pd = self.cpu_power_down && have_cpu_mem && can_power_down(Domain::Cpu); - // On C6 the CPU cannot survive a TOP power-down, so pd_top implies - // pd_cpu (both go through the software CPU-retention wake stub). + // The CPU can't survive a TOP power-down, so pd_top implies pd_cpu. self.pd_flags.set_pd_top(top_pd); self.pd_flags.set_pd_cpu(cpu_pd || top_pd); } @@ -914,18 +908,16 @@ impl RtcSleepConfig { // Start entry into sleep mode - // Arm regDMA retention of the TOP-domain peripherals. Must happen after - // the power config write above (which resets the backup-enable bits) and - // before the sleep request. + // Arm regDMA TOP-domain retention: after the power config write above + // (which resets the backup-enable bits) and before the sleep request. if !self.deep && self.pd_flags.pd_top() && !self.top_retention_mem.is_null() { let system_memory = unsafe { &mut *self.top_retention_mem }; crate::rtc_cntl::retention::enable_top_retention(system_memory); } if !self.deep && self.pd_flags.pd_cpu() && !self.cpu_retention_mem.is_null() { - // CPU power-down light sleep: save state, sleep, and resume here - // with it restored. The pointer is non-null whenever apply() sets - // pd_cpu, as that only happens via the buffer-carrying builders. + // CPU power-down light sleep: save state, sleep, resume with it + // restored. Non-null whenever apply() sets pd_cpu. let memory = unsafe { &mut *self.cpu_retention_mem }; unsafe { crate::rtc_cntl::cpu_retention::sleep_with_cpu_retention(memory); @@ -945,9 +937,9 @@ impl RtcSleepConfig { } } - // After a TOP power-down, TIMG0 (in the TOP domain, not retained) comes - // back with its flashboot watchdog armed. Disable it, like IDF's - // misc_modules_wake_prepare(), or it resets the chip shortly after. + // After a TOP power-down TIMG0 (not retained) comes back with its + // flashboot watchdog armed; disable it (like IDF's + // misc_modules_wake_prepare()) or it soon resets the chip. if !self.deep && self.pd_flags.pd_top() { let tg0 = crate::peripherals::TIMG0::regs(); tg0.wdtwprotect().write(|w| unsafe { w.bits(0x50D8_3AA1) }); diff --git a/esp-hal/src/spi/master/low_level/mod.rs b/esp-hal/src/spi/master/low_level/mod.rs index b1cbb03a140..9da4a5037a0 100644 --- a/esp-hal/src/spi/master/low_level/mod.rs +++ b/esp-hal/src/spi/master/low_level/mod.rs @@ -47,12 +47,18 @@ mod version; pub(super) struct SpiWrapper<'d> { pub(super) spi: AnySpi<'d>, _guard: PeripheralGuard, - // Active keeps `TOP` powered; `Spi::with_retention_memory` swaps to retained. + // Active keeps `TOP` powered; `with_retention_memory` swaps to retained. #[cfg(esp32c6)] pub(super) power: crate::rtc_cntl::retention::PowerManagement< 'd, crate::rtc_cntl::retention::SpiRetentionMemory, >, + // While retention is armed, hold the function clock on so the config + // registers stay accessible to regDMA at TOP power-down/restore. The driver + // otherwise only enables it transiently around config writes, leaving it + // gated at sleep entry, which makes regDMA back up/restore zeros. + #[cfg(esp32c6)] + pub(super) _retention_clock: Option, } impl<'d> SpiWrapper<'d> { @@ -63,6 +69,8 @@ impl<'d> SpiWrapper<'d> { _guard: PeripheralGuard::new(p), #[cfg(esp32c6)] power: crate::rtc_cntl::retention::PowerManagement::new(), + #[cfg(esp32c6)] + _retention_clock: None, }; // Initialize state @@ -109,6 +117,8 @@ impl Drop for SpiWrapper<'_> { } } +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub(super) struct SpiClockGuard { clock: SpiInstance, } diff --git a/esp-hal/src/spi/master/mod.rs b/esp-hal/src/spi/master/mod.rs index 4c0f28c07db..ba101ad8c70 100644 --- a/esp-hal/src/spi/master/mod.rs +++ b/esp-hal/src/spi/master/mod.rs @@ -996,12 +996,17 @@ where Dm: DriverMode, { /// Retain this SPI's config registers in `mem` across a `TOP` power-down in - /// light sleep. While active the driver keeps `TOP` powered; this drops that - /// lock and lets regDMA save/restore the config so `TOP` can power down. + /// light sleep, dropping the lock that would otherwise keep `TOP` powered. #[cfg(esp32c6)] #[instability::unstable] pub fn with_retention_memory(mut self, mem: &'d mut SpiRetentionMemory) -> Self { let base = self.driver().regs() as *const _ as usize as u32; + // Keep the function clock on for the retention lifetime: the SPI config + // registers are only reachable while it runs, so regDMA would otherwise + // back up/restore zeros at a `TOP` power-down (unlike UART/I2C, whose + // config sits behind the always-held APB clock). + let info = self.spi.info(); + self.spi._retention_clock = Some(SpiClockGuard::new(info)); self.spi.power.retain(mem, base); self } diff --git a/esp-hal/src/uart/mod.rs b/esp-hal/src/uart/mod.rs index 77c7f01cdd6..3fb5797ebf7 100644 --- a/esp-hal/src/uart/mod.rs +++ b/esp-hal/src/uart/mod.rs @@ -617,7 +617,7 @@ pub struct UartRx<'d, Dm: DriverMode> { phantom: PhantomData, guard: PeripheralGuard, peri_clock_guard: UartClockGuard<'d>, - // Active keeps `TOP` powered; `Uart::with_retention_memory` swaps to retained. + // Active keeps `TOP` powered; `with_retention_memory` swaps to retained. #[cfg(esp32c6)] power: crate::rtc_cntl::retention::PowerManagement<'d, UartRetentionMemory>, // Receiving data continuously, the peripheral can't let the system sleep. @@ -1868,9 +1868,8 @@ where } /// Retain this UART's config registers in `mem` across a `TOP` power-down in - /// light sleep. While active the driver keeps `TOP` powered; this drops that - /// lock and lets regDMA save/restore the config so `TOP` can power down. The - /// console/log UART is retained automatically and does not need this. + /// light sleep, dropping the lock that would otherwise keep `TOP` powered. + /// The console/log UART is retained automatically and does not need this. #[cfg(esp32c6)] #[instability::unstable] pub fn with_retention_memory(mut self, mem: &'d mut UartRetentionMemory) -> Self { diff --git a/qa-test/src/bin/sleep_timer_powerdown.rs b/qa-test/src/bin/sleep_timer_powerdown.rs index 38bd9f46ff4..72a83247475 100644 --- a/qa-test/src/bin/sleep_timer_powerdown.rs +++ b/qa-test/src/bin/sleep_timer_powerdown.rs @@ -1,23 +1,23 @@ //! Timer-woken light sleep with the CPU/TOP power domains powered down. //! //! Three policies for the same timer wakeup: clock-gated (plain light sleep), -//! cpu-powerdown ([`with_cpu_power_down`], CPU state saved in software) and -//! top-powerdown ([`with_top_power_down`], whole `TOP` domain off via regDMA; -//! also powers the CPU down on the C6). Each round prints the time slept and -//! [`cpu_power_down_wake_count`], which only advances when the CPU lost power. +//! cpu-powerdown (CPU state saved in software) and top-powerdown (whole `TOP` +//! domain off via regDMA, also powering the CPU down). Each round prints the +//! time slept and `cpu_power_down_wake_count`, which advances only when the CPU +//! lost power. //! //! It also proves the design end to end: //! -//! - **Negative control**: an un-retained `TOP` register (I2C0 `SCL_LOW_PERIOD`) survives a -//! clock-gated sleep; once its driver is dropped (releasing the power-domain lock that would -//! otherwise block the power-down) a `top-powerdown` wipes it and the CPU-power-down counter -//! advances - direct evidence the domain lost power. -//! - **Safety net**: while UART1 is active and un-retained it holds a `TOP` power-domain lock, so -//! `top-powerdown` degrades to clock-gating (the counter stays put). -//! - **Opt-in retention**: UART1/I2C0/SPI2 are retained, then a config register of each is -//! confirmed to survive `top-powerdown`. +//! - **Negative control**: a sentinel stamped into an un-retained `TOP` register (I2C0 +//! `SCL_LOW_PERIOD`) survives a clock-gated sleep, but a `top-powerdown` (after the driver is +//! dropped) wipes it and the CPU counter advances - the domain lost power. The I2C0 clock is +//! briefly re-enabled afterwards so the register can be read back validly. +//! - **Safety net**: an active, un-retained UART1 holds a `TOP` lock, so `top-powerdown` degrades +//! to clock-gating (the counter stays put). +//! - **Opt-in retention**: UART1/I2C0/SPI2 are retained and a config register of each is confirmed +//! to survive `top-powerdown`. //! -//! GPIO5 is high while awake, low while asleep, to bracket each sleep for a +//! GPIO5 is high while awake, low while asleep, to bracket each sleep on a //! current meter / logic analyzer. //% CHIP_FILTER: esp32c6 @@ -31,6 +31,7 @@ use esp_hal::{ gpio::{Level, Output, OutputConfig}, i2c::master::{Config as I2cConfig, I2c, I2cRetentionMemory}, main, + peripherals::SYSTEM, rtc_cntl::{ Rtc, cpu_retention::{CpuRetentionMemory, SystemRetentionMemory, cpu_power_down_wake_count}, @@ -111,14 +112,18 @@ fn main() -> ! { println!("up and running!"); // Negative control: I2C0's SCL_LOW_PERIOD (a TOP register) survives a - // clock-gated sleep, but once its driver is dropped a top-powerdown wipes - // it. A live driver holds a TOP power-domain lock (see the safety net - // below), so it has to be released first for the domain to actually power - // down - which is exactly what proves the domain lost power. + // clock-gated sleep, but once its driver is dropped (releasing its TOP lock) + // a top-powerdown wipes it - proof the domain lost power. const I2C0_SCL_LOW_PERIOD: *const u32 = 0x6000_4000 as *const u32; let i2c0 = I2c::new(peripherals.I2C0, I2cConfig::default()).unwrap(); - // SAFETY: driver alive, register readable. - let scl_configured = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; + // Stamp a recognizable sentinel into the TOP register so its fate is obvious + // in the log; the exact value doesn't matter, only whether it survives. + const SENTINEL: u32 = 0x155; + // SAFETY: driver alive, register writable and readable. + let scl_configured = unsafe { + (I2C0_SCL_LOW_PERIOD as *mut u32).write_volatile(SENTINEL); + I2C0_SCL_LOW_PERIOD.read_volatile() + }; marker.set_low(); lpwr.sleep( @@ -129,8 +134,9 @@ fn main() -> ! { // Read while the driver is still alive (TOP stayed powered, so it survives). let scl_after_clock_gate = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; - // Drop the driver so its TOP power-domain lock is released and the domain can - // actually power down (nothing else holds one yet). + // Drop the driver so its TOP lock is released and the domain can power down. + // This also gates the I2C0 clock, so its registers can't be read back until + // the clock is turned on again after the sleep. core::mem::drop(i2c0); let downs_before_nc = cpu_power_down_wake_count(); @@ -140,10 +146,19 @@ fn main() -> ! { &[&TimerWakeupSource::new(Duration::from_millis(EVENT_MS))], ); marker.set_high(); - // SAFETY: register readable; reads 0 now that TOP lost power (and reset it). - let scl_after_top_pd = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; let downs_after_nc = cpu_power_down_wake_count(); + // Re-enable just the I2C0 clock (no reset) before reading. With the clock + // gated the bus reads 0 regardless of retention, which would prove nothing; + // with it on, a reset value here means TOP genuinely lost the register's + // contents across the power-down. + SYSTEM::regs() + .i2c0_conf() + .modify(|_, w| w.i2c0_clk_en().set_bit()); + // SAFETY: register readable now the clock is on; reads its reset value (0) + // because TOP lost power and reset it. + let scl_after_top_pd = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; + println!( "negative-control I2C0 SCL_LOW_PERIOD: configured = {:#x}", scl_configured @@ -169,8 +184,8 @@ fn main() -> ! { downs_after_nc ); - // UART1 is live but not yet retained, so it holds a TOP power-domain lock: - // top-powerdown must degrade to clock-gating (counter must not advance). + // UART1 is live but not retained, so it holds a TOP lock: top-powerdown must + // degrade to clock-gating (counter must not advance). const UART1_CLKDIV: *const u32 = 0x6000_1014 as *const u32; let uart1 = Uart::new(peripherals.UART1, UartConfig::default()).unwrap(); @@ -198,8 +213,8 @@ fn main() -> ! { } ); - // Retain UART1: drops the lock and stores the memory in the driver, so its - // registers are saved/restored around the power-down. Kept alive for the proof. + // Retain UART1: drops the lock so its registers are saved/restored around the + // power-down. Kept alive for the proof. let _uart1 = uart1.with_retention_memory(mk_static!(UartRetentionMemory, UartRetentionMemory::new())); // SAFETY: driver alive, register readable. From 2934a9cb9571674b0a538a27ab01b885f8ac129e Mon Sep 17 00:00:00 2001 From: Juraj Sadel Date: Thu, 9 Jul 2026 12:32:35 +0200 Subject: [PATCH 4/8] add h2 and clean --- esp-hal/src/i2c/master/mod.rs | 12 +- esp-hal/src/rtc_cntl/cpu_retention.rs | 95 +- esp-hal/src/rtc_cntl/mod.rs | 6 +- esp-hal/src/rtc_cntl/power_domain.rs | 6 +- esp-hal/src/rtc_cntl/retention.rs | 819 +++++++++++------- esp-hal/src/rtc_cntl/retention/esp32c6.rs | 126 +++ esp-hal/src/rtc_cntl/retention/esp32h2.rs | 131 +++ esp-hal/src/rtc_cntl/sleep/esp32c6.rs | 86 +- esp-hal/src/rtc_cntl/sleep/esp32h2.rs | 92 +- esp-hal/src/spi/master/low_level/mod.rs | 8 +- esp-hal/src/spi/master/mod.rs | 4 +- esp-hal/src/uart/mod.rs | 23 +- .../src/_build_script_utils.rs | 5 + .../src/_generated_esp32.rs | 3 + .../src/_generated_esp32c2.rs | 3 + .../src/_generated_esp32c3.rs | 3 + .../src/_generated_esp32c5.rs | 3 + .../src/_generated_esp32c6.rs | 3 + .../src/_generated_esp32c61.rs | 3 + .../src/_generated_esp32h2.rs | 3 + .../src/_generated_esp32p4.rs | 3 + .../src/_generated_esp32s2.rs | 3 + .../src/_generated_esp32s3.rs | 3 + esp-metadata/devices/esp32c6/soc.toml | 3 + esp-metadata/devices/esp32h2/soc.toml | 3 + esp-metadata/src/cfg.rs | 4 + qa-test/src/bin/sleep_timer_powerdown.rs | 2 +- 27 files changed, 986 insertions(+), 469 deletions(-) create mode 100644 esp-hal/src/rtc_cntl/retention/esp32c6.rs create mode 100644 esp-hal/src/rtc_cntl/retention/esp32h2.rs diff --git a/esp-hal/src/i2c/master/mod.rs b/esp-hal/src/i2c/master/mod.rs index 836d3ec1534..d38c9b350e1 100644 --- a/esp-hal/src/i2c/master/mod.rs +++ b/esp-hal/src/i2c/master/mod.rs @@ -153,7 +153,7 @@ mod low_level; pub use low_level::{AnyI2c, Instance}; use low_level::{Driver, I2cClockGuard}; -#[cfg(esp32c6)] +#[cfg(sleep_pd_retention)] #[instability::unstable] pub use crate::rtc_cntl::retention::I2cRetentionMemory; @@ -685,7 +685,7 @@ pub struct I2c<'d, Dm: DriverMode> { guard: PeripheralGuard, config: DriverConfig, // Active keeps `TOP` powered; `with_retention_memory` swaps to retained. - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] power: crate::rtc_cntl::retention::PowerManagement< 'd, crate::rtc_cntl::retention::I2cRetentionMemory, @@ -746,7 +746,7 @@ impl<'d> I2c<'d, Blocking> { sda_pin, scl_pin, }, - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] power: crate::rtc_cntl::retention::PowerManagement::new(), }; @@ -771,7 +771,7 @@ impl<'d> I2c<'d, Blocking> { phantom: PhantomData, guard: self.guard, config: self.config, - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] power: self.power, } } @@ -865,7 +865,7 @@ impl<'d> I2c<'d, Async> { phantom: PhantomData, guard: self.guard, config: self.config, - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] power: self.power, } } @@ -1056,7 +1056,7 @@ where /// Retain this I2C's config registers in `mem` across a `TOP` power-down in /// light sleep, dropping the lock that would otherwise keep `TOP` powered. - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] #[instability::unstable] pub fn with_retention_memory(mut self, mem: &'d mut I2cRetentionMemory) -> Self { let base = self.i2c.info().regs() as *const RegisterBlock as usize as u32; diff --git a/esp-hal/src/rtc_cntl/cpu_retention.rs b/esp-hal/src/rtc_cntl/cpu_retention.rs index 42599082aff..af4f5205113 100644 --- a/esp-hal/src/rtc_cntl/cpu_retention.rs +++ b/esp-hal/src/rtc_cntl/cpu_retention.rs @@ -1,24 +1,20 @@ -//! CPU power-down retention during light sleep (ESP32-C6). +//! CPU power-down retention during light sleep (RISC-V PMU chips). //! -//! When the C6 powers down `pd_cpu` in light sleep the CPU loses all state. -//! regDMA can't reach the register file/CSRs, so they are saved/restored in -//! software. Backing RAM -//! ([`CpuRetentionMemory`]) is caller-owned, opt-in via -//! [`RtcSleepConfig::with_cpu_power_down`]; without it the CPU is clock-gated. +//! When `pd_cpu` powers down, the CPU loses all state and regDMA can't reach the +//! register file/CSRs, so they are saved/restored in software in three parts: +//! critical registers (GP + machine CSRs) in assembly via a setjmp/longjmp-style +//! trick, non-critical CSRs via `csrr`/`csrw`, and CPU-domain device registers +//! (`INTPRI`, `PLIC`/`CLINT`, cache). Backing RAM ([`CpuRetentionMemory`]) is +//! caller-owned, opt-in via +//! [`RtcSleepConfig::with_cpu_power_down`](crate::rtc_cntl::sleep::RtcSleepConfig::with_cpu_power_down). //! -//! [`RtcSleepConfig::with_cpu_power_down`]: crate::rtc_cntl::sleep::RtcSleepConfig::with_cpu_power_down -//! -//! Save/restore has three parts: critical registers (GP + machine CSRs) in -//! assembly via a setjmp/longjmp-style trick; non-critical CSRs via `csrr`/ -//! `csrw`; and CPU-domain device registers (`INTPRI`, `PLIC`/`CLINT`, cache). -//! -//! Everything runs from IRAM (`.rwtext`): the ROM jumps to the wake stub with -//! the flash cache lost, so every function is `#[ram]` and must not call -//! flash-resident code until the cache config is restored. +//! The logic is chip-agnostic - only the device-register base addresses are +//! per-chip data (from the `retention::chip` module). Everything runs from IRAM +//! (`.rwtext`): the ROM wakes with the flash cache lost, so no `#[ram]` function +//! may call flash-resident code until the cache config is restored. -// Software register retention here mirrors ESP-IDF's `esp_sleep_cpu_retention()`. -// Ported from ESP-IDF `v5.4` `esp32c6` `sleep_cpu.c`, `sleep_cpu_asm.S`, -// `rvsleep-frames.h`. +// Mirrors ESP-IDF `v5.4` `esp_sleep_cpu_retention()` (`sleep_cpu.c`, +// `sleep_cpu_asm.S`, `rvsleep-frames.h`). use core::sync::atomic::{AtomicU32, Ordering}; @@ -364,68 +360,78 @@ const fn total_words(regions: &[Region]) -> usize { words } -// Interrupt matrix priority registers (`INTPRI`, base 0x600C_5000). +// Per-chip base addresses (the region layout below is chip-agnostic). +use crate::rtc_cntl::retention::{ + CACHE_BASE, + CLINT_MINT_BASE, + CLINT_UINT_BASE, + INTPRI_BASE, + PLIC_MX_BASE, + PLIC_UX_BASE, +}; + +// Interrupt matrix priority registers (`INTPRI`). const INTPRI_REGIONS: [Region; 2] = [ // INTPRI_CORE0_CPU_INT_ENABLE_REG ..= INTPRI_RND_ECO_LOW_REG Region { - start: 0x600C_5000, + start: INTPRI_BASE, words: 45, }, // INTPRI_RND_ECO_HIGH_REG Region { - start: 0x600C_53FC, + start: INTPRI_BASE + 0x3FC, words: 1, }, ]; -// L1 cache control (`EXTMEM`, base 0x600C_8000). +// L1 cache control (`EXTMEM`/`CACHE`). const CACHE_REGIONS: [Region; 2] = [ - // EXTMEM_L1_CACHE_CTRL_REG + // *_L1_CACHE_CTRL_REG Region { - start: 0x600C_8004, + start: CACHE_BASE + 0x4, words: 1, }, - // EXTMEM_L1_CACHE_WRAP_AROUND_CTRL_REG + // *_L1_CACHE_WRAP_AROUND_CTRL_REG Region { - start: 0x600C_8020, + start: CACHE_BASE + 0x20, words: 1, }, ]; -// PLIC machine/user interrupt controllers (bases 0x2000_1000 / 0x2000_1400). +// PLIC machine/user interrupt controllers. const PLIC_REGIONS: [Region; 4] = [ // PLIC_MXINT_ENABLE_REG ..= PLIC_MXINT_CLAIM_REG Region { - start: 0x2000_1000, + start: PLIC_MX_BASE, words: 38, }, // PLIC_MXINT_CONF_REG Region { - start: 0x2000_13FC, + start: PLIC_MX_BASE + 0x3FC, words: 1, }, // PLIC_UXINT_ENABLE_REG ..= PLIC_UXINT_CLAIM_REG Region { - start: 0x2000_1400, + start: PLIC_UX_BASE, words: 38, }, // PLIC_UXINT_CONF_REG Region { - start: 0x2000_17FC, + start: PLIC_UX_BASE + 0x3FC, words: 1, }, ]; -// CLINT machine/user timers (bases 0x2000_1800 / 0x2000_1C00). +// CLINT machine/user timers. const CLINT_REGIONS: [Region; 2] = [ // CLINT_MINT_SIP_REG ..= CLINT_MINT_MTIMECMP_H_REG Region { - start: 0x2000_1800, + start: CLINT_MINT_BASE, words: 6, }, // CLINT_UINT_SIP_REG ..= CLINT_UINT_UTIMECMP_H_REG Region { - start: 0x2000_1C00, + start: CLINT_UINT_BASE, words: 6, }, ]; @@ -569,13 +575,23 @@ fn do_cpu_retention() { /// CPU-power-down light sleep with software register retention, wrapping the /// sleep trigger in save/restore. /// +/// `top` is the TOP-domain regDMA store when the `TOP` domain is also powered +/// down (null otherwise). On chips whose PAU powers down with `TOP` +/// ([`SystemRetentionMemory`]'s software-triggered restore), the peripherals - +/// including the flash SPI controller - must be restored here in RAM before this +/// function returns to flash-resident code; on hardware-restore chips +/// (and when `top` is null) that call is a no-op. +/// /// # Safety /// /// The PMU must already be configured for a `pd_cpu` light sleep, stopping the -/// CPU must be safe, and `mem` must stay valid across the sleep. +/// CPU must be safe, and `mem`/`top` must stay valid across the sleep. // Mirrors ESP-IDF's `esp_sleep_cpu_retention()`. #[ram] -pub(crate) unsafe fn sleep_with_cpu_retention(mem: &mut CpuRetentionMemory) { +pub(crate) unsafe fn sleep_with_cpu_retention( + mem: &mut CpuRetentionMemory, + top: *mut crate::rtc_cntl::retention::SystemRetentionMemory, +) { unsafe { RV_CORE_CRITICAL_REGS_FRAME = mem.critical.as_mut_ptr(); @@ -589,6 +605,13 @@ pub(crate) unsafe fn sleep_with_cpu_retention(mem: &mut CpuRetentionMemory) { do_cpu_retention(); + // Software-triggered regDMA restore (TOP powered down): bring the TOP + // peripherals - crucially the flash SPI controller - back first, while + // still running from RAM. No-op on hardware-restore chips / cpu-only pd. + if !top.is_null() { + crate::rtc_cntl::retention::restore_top_retention(&mut *top); + } + // Restore in reverse order; the cache config must come back before we // return to flash-resident code. restore_noncritical(mem.noncritical.as_ptr()); diff --git a/esp-hal/src/rtc_cntl/mod.rs b/esp-hal/src/rtc_cntl/mod.rs index c88f7d37de9..cbba0fa42d8 100644 --- a/esp-hal/src/rtc_cntl/mod.rs +++ b/esp-hal/src/rtc_cntl/mod.rs @@ -125,15 +125,15 @@ use crate::{peripherals::RTC_TIMER, system::Cpu, time::Duration}; pub mod sleep; // Power-domain locks that keep a domain powered across light sleep. -#[cfg(esp32c6)] +#[cfg(sleep_pd_retention)] pub(crate) mod power_domain; // regDMA-based register retention of the TOP domain's peripherals. -#[cfg(esp32c6)] +#[cfg(sleep_pd_retention)] pub(crate) mod retention; // Software CPU-register retention for CPU power-down. -#[cfg(esp32c6)] +#[cfg(sleep_pd_retention)] pub mod cpu_retention; #[cfg_attr(esp32, path = "rtc/esp32.rs")] diff --git a/esp-hal/src/rtc_cntl/power_domain.rs b/esp-hal/src/rtc_cntl/power_domain.rs index ba1b0f7c3b1..61fd7c0a45c 100644 --- a/esp-hal/src/rtc_cntl/power_domain.rs +++ b/esp-hal/src/rtc_cntl/power_domain.rs @@ -1,4 +1,4 @@ -//! Power-domain locks for light sleep (ESP32-C6). +//! Power-domain locks for light sleep (RISC-V PMU chips). //! //! An active, un-retained peripheral holds a [`PowerDomainLock`]. Unlike a //! [`WakeLock`](crate::rtc_cntl::WakeLock) it doesn't prevent sleep, only the @@ -43,8 +43,8 @@ impl Drop for PowerDomainLock { } } -/// Whether `domain` may be powered down. On the C6 powering `TOP` down also -/// tears down the CPU domain, so `Top` requires both to be free. +/// Whether `domain` may be powered down. Powering `TOP` down also tears down the +/// CPU domain, so `Top` requires both to be free. pub(crate) fn can_power_down(domain: Domain) -> bool { let blocked = match domain { Domain::Cpu => LOCKS[Domain::Cpu as usize].load(Ordering::Acquire) != 0, diff --git a/esp-hal/src/rtc_cntl/retention.rs b/esp-hal/src/rtc_cntl/retention.rs index d825526848f..210f8d0404c 100644 --- a/esp-hal/src/rtc_cntl/retention.rs +++ b/esp-hal/src/rtc_cntl/retention.rs @@ -1,14 +1,17 @@ -//! Register DMA (regDMA) based register retention (ESP32-C6). +//! Register DMA (regDMA) based register retention. //! -//! The PAU's regDMA engine backs peripheral registers up to RAM and restores -//! them while the `TOP` domain is powered down in light sleep, walking a linked -//! list of [`RegdmaLink`] nodes on PAU entry link 0 with no CPU involvement. -//! `sys_periph` builds the core register set into a caller-owned -//! [`SystemRetentionMemory`]; [`enable_top_retention`] chains it with any opt-in -//! peripheral entries before arming the link. Without it `TOP` is clock-gated. +//! The PAU's regDMA engine backs the `TOP`-domain peripheral registers up to RAM +//! and restores them across a `TOP` power-down in light sleep, walking a linked +//! list of [`RegdmaLink`] nodes on PAU entry link 0. Arming builds the core set +//! into a caller-owned [`SystemRetentionMemory`], chains any opt-in peripheral +//! entries and programs the link. +//! +//! All logic here is chip-agnostic; the only per-chip input is register data +//! (the `OPS` program and the base addresses), which lives in the `chip` +//! submodule - one data file per chip. Adding a chip is a new data file. -// References (ESP-IDF `v5.4`): `soc/regdma.h`, `hal/esp32c6/pau_ll.h`, -// `hal/esp32c6/pau_hal.c`, `esp_hw_support/port/pau_regdma.c`. +// References (ESP-IDF `v5.4`): `soc/regdma.h`, `hal//pau_ll.h`, +// `hal//pau_hal.c`, `esp_hw_support/port/pau_regdma.c`. use core::{ marker::PhantomData, @@ -17,10 +20,32 @@ use core::{ }; use esp_sync::NonReentrantMutex; +use procmacros::ram; use crate::{ peripherals::{PAU, PCR, PMU}, - rtc_cntl::power_domain::{Domain, PowerDomainLock}, + rtc_cntl::{ + cpu_retention::CpuRetentionMemory, + power_domain::{Domain, PowerDomainLock, can_power_down}, + }, +}; + +// Per-chip register data (base addresses, region sizes, the SYS_PERIPH program +// and the CPU-domain device-register bases). Selected by target; consumed only +// by the chip-agnostic logic here and (via the re-export below) `cpu_retention`. +#[cfg_attr(esp32c6, path = "retention/esp32c6.rs")] +#[cfg_attr(esp32h2, path = "retention/esp32h2.rs")] +mod chip; + +// The CPU-domain device-register bases live in the same per-chip data module; +// re-export them so `cpu_retention` can read them while `chip` stays private. +pub(crate) use chip::{ + CACHE_BASE, + CLINT_MINT_BASE, + CLINT_UINT_BASE, + INTPRI_BASE, + PLIC_MX_BASE, + PLIC_UX_BASE, }; // Bit layout of `regdma_link_head_t` (see ESP-IDF `regdma.h`): @@ -214,15 +239,13 @@ fn build_i2c_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { } // GPSPI2 config-register retention. ESP-IDF v5.4 `spi_periph.c` -// `spi2_regs_retention`, `spi_reg.h`. IDF's restore-time re-set of the -// TRANS_DONE/DMA_SEG_TRANS_DONE interrupt bits is omitted: esp-hal only powers -// `TOP` down on an idle bus, so it would only inject a spurious completion IRQ. +// `spi2_regs_retention`, `spi_reg.h`. const SPI_CMD_OFF: u32 = 0x00; // SPI_CMD_REG: ADDR_MAP window base /// Registers retained (set bits in [`SPI_REGS_MAP`]). const SPI_RETENTION_REGS_CNT: u32 = 12; /// `spi_regs_map[4]`: config registers in the `CMD..SLAVE` window. const SPI_REGS_MAP: [u32; 4] = [0x0000_31ff, 0x0100_0000, 0, 0]; -/// A single ADDR_MAP over the config registers, matching ESP-IDF. +/// A single ADDR_MAP over the config registers. const SPI_NODE_COUNT: usize = 1; /// Build the SPI retention sequence for `base` into `nodes`, backing the @@ -230,8 +253,8 @@ const SPI_NODE_COUNT: usize = 1; /// /// The config registers are only reachable while the SPI function clock runs. /// `Spi::with_retention_memory` holds that clock for the retention lifetime, so -/// they stay accessible at both backup and restore and this single ADDR_MAP is -/// all that is needed (as in ESP-IDF's `spi2_regs_retention`). +/// they stay accessible at both backup and restore. +// `spi2_regs_retention`) fn build_spi_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { nodes[0] = RegdmaLink::addr_map( base + SPI_CMD_OFF, @@ -345,7 +368,6 @@ fn arm_link(core: &mut [RegdmaLink]) -> u32 { (node.head, node.len, node.next) }; let seg = unsafe { core::slice::from_raw_parts_mut(seg_head, seg_len) }; - // SAFETY: `tail` is the last node of the previous segment. unsafe { (*tail).next = seg[0].addr(); (*tail).head &= !HEAD_EOF_BIT; @@ -355,8 +377,6 @@ fn arm_link(core: &mut [RegdmaLink]) -> u32 { } }); - // Terminate the final node. - // SAFETY: `tail` points at the last node of the last segment. unsafe { (*tail).next = 0; (*tail).head |= HEAD_EOF_BIT; @@ -518,9 +538,193 @@ impl defmt::Format for PowerManagement<'_, M> { } } -/// Caller-owned backing store for the ESP32-C6 TOP-domain system-peripheral -/// register set (PCR, interrupt matrix, HP system, TEE/APM, IO MUX, GPIO matrix, -/// flash SPI mem, console UART and SysTimer). +/// One step of a chip's TOP-domain retention program (`chip::OPS`), expanded +/// into PAU regDMA nodes by [`sys_periph::build_link`]. `Write`/`Wait` are +/// restore-only (they re-apply state the backup can't capture, e.g. unlocking +/// TEE/APM or pulsing a clock-update bit). +// A given chip may not use every variant (only the H2 needs `Wait`). +#[allow(dead_code)] +#[derive(Clone, Copy)] +enum SysOp { + /// Back up/restore `count` consecutive registers starting at `base`. + Continuous { base: u32, count: u32 }, + /// Back up `count` words from `backup`, restore them to `restore` (e.g. + /// GPIO output-enable via the W1TS register). + ContinuousSplit { + backup: u32, + restore: u32, + count: u32, + }, + /// Restore-only masked write of `value` to `addr`. + Write { addr: u32, value: u32, mask: u32 }, + /// Restore-only poll of `addr` until `(reg & mask) == value`. + Wait { addr: u32, value: u32, mask: u32 }, + /// The shared console-UART config sequence for the UART based at `base`. + Uart { base: u32 }, + /// The shared SysTimer save/restore sequence based at `base`. + Systimer { base: u32 }, +} + +/// PAU nodes emitted for one [`SysOp`]. +const fn op_nodes(op: &SysOp) -> usize { + match op { + SysOp::Continuous { .. } + | SysOp::ContinuousSplit { .. } + | SysOp::Write { .. } + | SysOp::Wait { .. } => 1, + SysOp::Uart { .. } => UART_NODE_COUNT, + SysOp::Systimer { .. } => SYSTIMER_NODE_COUNT, + } +} + +/// RAM buffer words consumed by one [`SysOp`]. +const fn op_words(op: &SysOp) -> usize { + match op { + SysOp::Continuous { count, .. } | SysOp::ContinuousSplit { count, .. } => *count as usize, + SysOp::Uart { .. } => UART_RETENTION_REGS_CNT as usize, + SysOp::Systimer { .. } => SYSTIMER_CONT_WORDS, + SysOp::Write { .. } | SysOp::Wait { .. } => 0, + } +} + +/// Total PAU nodes an op list expands to (sizes [`SystemRetentionMemory`]). +const fn ops_node_count(ops: &[SysOp]) -> usize { + let mut n = 0; + let mut i = 0; + while i < ops.len() { + n += op_nodes(&ops[i]); + i += 1; + } + n +} + +/// Total RAM buffer words an op list needs (sizes [`SystemRetentionMemory`]). +const fn ops_buf_words(ops: &[SysOp]) -> usize { + let mut w = 0; + let mut i = 0; + while i < ops.len() { + w += op_words(&ops[i]); + i += 1; + } + w +} + +// SysTimer register offsets and bit masks (shared; only the base differs per +// chip). Offsets/masks from ESP-IDF v5.4 `systimer_reg.h`; the save/restore +// sequence mirrors `systimer_regs_retention[]`. +const ST_UNIT_UPDATE: u32 = 1 << 30; +const ST_UNIT_VALUE_VALID: u32 = 1 << 29; +const ST_UNIT_LOAD: u32 = 1 << 0; +const ST_COMP_LOAD: u32 = 1 << 0; +const ST_TARGET_PERIOD_MODE: u32 = 1 << 30; +/// TARGET0_HI ..= TARGET2_CONF, i.e. all three targets' hi/lo/conf. +const ST_TARGETS_LEN: u32 = 9; +/// One node per SysTimer step of `build_systimer_seq`. +const SYSTIMER_NODE_COUNT: usize = 19; +/// SysTimer CONTINUOUS words: unit0/1 value (2+2), targets (9), conf, int_ena. +const SYSTIMER_CONT_WORDS: usize = 2 + 2 + ST_TARGETS_LEN as usize + 1 + 1; + +/// Build the SysTimer retention sequence for the timer at `base` into `nodes`, +/// drawing its RAM from `buf_base` starting at word `start_word`. Fills exactly +/// [`SYSTIMER_NODE_COUNT`] nodes and consumes [`SYSTIMER_CONT_WORDS`] words. +/// +/// Backup latches each unit's counter (UPDATE + wait for VALUE_VALID) and reads +/// it; restore loads it back and triggers a load. The value is read from +/// VALUE_HI/LO but restored into LOAD_HI/LO, hence the split backup/restore +/// addresses. +fn build_systimer_seq(base: u32, nodes: &mut [RegdmaLink], buf_base: *mut u32, start_word: usize) { + let st_conf = base; + let st_unit0_op = base + 0x04; + let st_unit1_op = base + 0x08; + let st_unit0_load_hi = base + 0x0C; + let st_unit1_load_hi = base + 0x14; + let st_target0_hi = base + 0x1C; + let st_target0_conf = base + 0x34; + let st_target1_conf = base + 0x38; + let st_target2_conf = base + 0x3C; + let st_unit0_value_hi = base + 0x40; + let st_unit1_value_hi = base + 0x48; + let st_comp0_load = base + 0x50; + let st_comp1_load = base + 0x54; + let st_comp2_load = base + 0x58; + let st_unit0_load = base + 0x5C; + let st_unit1_load = base + 0x60; + let st_int_ena = base + 0x64; + + let mut word = start_word; + let mut alloc = |len: u32| -> u32 { + let mem = unsafe { buf_base.add(word) } as u32; + word += len as usize; + mem + }; + + let mut node = 0; + + // Per unit: latch + read value, then restore into load. + for (op, value_hi, load_hi, load) in [ + ( + st_unit0_op, + st_unit0_value_hi, + st_unit0_load_hi, + st_unit0_load, + ), + ( + st_unit1_op, + st_unit1_value_hi, + st_unit1_load_hi, + st_unit1_load, + ), + ] { + nodes[node] = RegdmaLink::write(op, ST_UNIT_UPDATE, ST_UNIT_UPDATE, false, true); + node += 1; + nodes[node] = RegdmaLink::wait(op, ST_UNIT_VALUE_VALID, ST_UNIT_VALUE_VALID, false, true); + node += 1; + let mem = alloc(2); + nodes[node] = RegdmaLink::continuous_split(value_hi, load_hi, mem, 2); + node += 1; + nodes[node] = RegdmaLink::write(load, ST_UNIT_LOAD, ST_UNIT_LOAD, true, false); + node += 1; + } + + // Comparator target values & periods. + let mem = alloc(ST_TARGETS_LEN); + nodes[node] = RegdmaLink::continuous(st_target0_hi, mem, ST_TARGETS_LEN); + node += 1; + for comp in [st_comp0_load, st_comp1_load, st_comp2_load] { + nodes[node] = RegdmaLink::write(comp, ST_COMP_LOAD, ST_COMP_LOAD, true, false); + node += 1; + } + // Re-arm period mode: clear+set for target0/1, clear for target2. + for target in [st_target0_conf, st_target1_conf] { + nodes[node] = RegdmaLink::write(target, 0, ST_TARGET_PERIOD_MODE, true, false); + node += 1; + nodes[node] = RegdmaLink::write( + target, + ST_TARGET_PERIOD_MODE, + ST_TARGET_PERIOD_MODE, + true, + false, + ); + node += 1; + } + nodes[node] = RegdmaLink::write(st_target2_conf, 0, ST_TARGET_PERIOD_MODE, true, false); + node += 1; + + // Work-enable and interrupt-enable state. + let mem = alloc(1); + nodes[node] = RegdmaLink::continuous(st_conf, mem, 1); + node += 1; + let mem = alloc(1); + nodes[node] = RegdmaLink::continuous(st_int_ena, mem, 1); + node += 1; + + debug_assert!(node == SYSTIMER_NODE_COUNT); + debug_assert!(word - start_word == SYSTIMER_CONT_WORDS); +} + +/// Caller-owned backing store for the TOP-domain system-peripheral register set +/// (PCR, interrupt matrix, HP system, TEE/APM, IO MUX, GPIO matrix, flash SPI +/// mem, console UART and SysTimer - see the chip's `OPS` retention program). /// /// The core state regDMA must retain for the `TOP` domain to power down at all; /// the caller opts in via [`RtcSleepConfig::with_top_power_down`]. Individual @@ -532,8 +736,8 @@ impl defmt::Format for PowerManagement<'_, M> { #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(C, align(4))] pub struct SystemRetentionMemory { - nodes: [RegdmaLink; sys_periph::NODE_COUNT], - buf: [u32; sys_periph::BUF_WORDS], + nodes: [RegdmaLink; ops_node_count(chip::OPS)], + buf: [u32; ops_buf_words(chip::OPS)], } #[instability::unstable] @@ -548,344 +752,325 @@ impl SystemRetentionMemory { #[instability::unstable] pub const fn new() -> Self { Self { - nodes: [RegdmaLink::EMPTY; sys_periph::NODE_COUNT], - buf: [0; sys_periph::BUF_WORDS], + nodes: [RegdmaLink::EMPTY; ops_node_count(chip::OPS)], + buf: [0; ops_buf_words(chip::OPS)], } } } -/// Arm regDMA retention of the TOP-domain peripherals for the upcoming light -/// sleep: program PAU entry link 0 and enable the backup phases. -/// -/// Must be called after the PMU power config (which resets the backup-enable -/// bits) and before the sleep request. Rebuilds the chain from the live registry -/// every time, so the PAU never walks a deregistered entry. -// Mirrors ESP-IDF's link setup + `pmu_sleep_enable_regdma_backup()` -// (active/sleep phases only). -pub(crate) fn enable_top_retention(mem: &mut SystemRetentionMemory) { - // pau_ll_enable_bus_clock: enable the regDMA bus clock and release its reset. +/// Enable the regDMA bus clock and release its reset (`pau_ll_enable_bus_clock`), +/// and bound the WAIT polling so a never-satisfied condition can't hang the +/// engine (`pau_hal_set_regdma_wait_timeout`, ESP-IDF `PAU_REGDMA_LINK_WAIT_*`). +// `#[ram]`: also called from the wake path (see `restore_top_retention`). +#[ram] +fn regdma_clock_and_timeout() { PCR::regs().regdma_conf().modify(|_, w| { w.regdma_clk_en().set_bit(); w.regdma_rst_en().clear_bit() }); - // pau_hal_set_regdma_wait_timeout: bound WAIT polling so a never-satisfied - // condition can't hang the engine (ESP-IDF PAU_REGDMA_LINK_WAIT_*). PAU::regs().regdma_bkp_conf().modify(|_, w| unsafe { w.link_tout_thres().bits(1000); w.read_interval().bits(32) }); +} + +/// Software-trigger a regDMA transfer of system link 0 and wait for it. `backup` +/// copies registers into RAM, else restores RAM back into registers. Used on +/// [`chip::SW_TRIGGER_REGDMA`] chips whose PAU powers off with `TOP`, so the PMU +/// can't drive the transfer. +// Mirrors ESP-IDF `pau_hal_start_regdma_system_link`. +// `#[ram]`: the restore runs on wake before the flash SPI controller is back. +#[ram] +fn sw_trigger_system_link(backup: bool) { + let pau = PAU::regs(); + // `start` must be asserted in its own write, after link_sel/to_mem are set; + // a combined write leaves the transfer's `done` never asserting. + pau.int_clr().write(|w| w.done().clear_bit_by_one()); + pau.regdma_conf() + .modify(|_, w| unsafe { w.link_sel().bits(0) }); + pau.regdma_conf().modify(|_, w| w.to_mem().bit(backup)); + pau.regdma_conf().modify(|_, w| w.start().set_bit()); + // Bounded safety net; WAIT nodes are separately bounded by the wait timeout. + for _ in 0..2_000_000u32 { + if pau.int_raw().read().done().bit_is_set() { + break; + } + } + pau.regdma_conf().modify(|_, w| w.start().clear_bit()); + pau.regdma_conf() + .modify(|_, w| unsafe { w.link_sel().bits(0) }); + pau.int_clr().write(|w| w.done().clear_bit_by_one()); +} + +/// Arm regDMA retention of the TOP-domain peripherals for the upcoming light +/// sleep: program PAU entry link 0 and start the backup. Must be called after +/// the PMU power config (which resets the backup-enable bits) and before the +/// sleep request; rebuilds the chain from the live registry each time. +/// +/// Where the PAU survives the `TOP` power-down the PMU drives backup/restore in +/// hardware. On [`chip::SW_TRIGGER_REGDMA`] chips the PAU powers off with `TOP`, +/// so the hardware backup is disabled and the backup is triggered in software +/// here, with [`restore_top_retention`] doing the restore on wake. +fn enable_top_retention(mem: &mut SystemRetentionMemory) { + regdma_clock_and_timeout(); - // Build the SYS_PERIPH list plus opt-in entries and program it as link 0. let head = arm_link(sys_periph::build_link(&mut mem.nodes, &mut mem.buf)); fence(Ordering::SeqCst); PAU::regs() .regdma_link_0_addr() .write(|w| unsafe { w.bits(head) }); - // pmu_sleep_enable_regdma_backup: back up active->sleep, restore sleep->active. let pmu = PMU::regs(); - pmu.hp_sleep_backup() - .modify(|_, w| w.hp_active2sleep_backup_en().set_bit()); - pmu.hp_active_backup() - .modify(|_, w| w.hp_sleep2active_backup_en().set_bit()); -} - -/// TOP-domain system-peripheral retention link: register regions for the core -/// peripherals lost when `TOP` powers down, in retention-priority order (system -/// clock first). -// Mirrors ESP-IDF's `SLEEP_RETENTION_MODULE_SYS_PERIPH` + `..._CLOCK_SYSTEM`. -// References (ESP-IDF `v5.4`): `soc/esp32c6/system_retention_periph.c`, -// `esp_hw_support/.../sleep_clock.c`, `esp_hw_support/sleep_system_peripheral.c`. + if chip::SW_TRIGGER_REGDMA { + // The PAU powers off with TOP: the PMU can't run the backup on the + // active->sleep transition, so disable it and back up in software now. + pmu.hp_sleep_backup() + .modify(|_, w| w.hp_active2sleep_backup_en().clear_bit()); + pmu.hp_active_backup() + .modify(|_, w| w.hp_sleep2active_backup_en().clear_bit()); + sw_trigger_system_link(true); + } else { + // pmu_sleep_enable_regdma_backup: back up active->sleep, restore + // sleep->active, both driven by the PMU in hardware. + pmu.hp_sleep_backup() + .modify(|_, w| w.hp_active2sleep_backup_en().set_bit()); + pmu.hp_active_backup() + .modify(|_, w| w.hp_sleep2active_backup_en().set_bit()); + } +} + +/// Restore the TOP-domain peripherals on wake for [`chip::SW_TRIGGER_REGDMA`] +/// chips, whose PAU lost its own configuration with the `TOP` power-down. +/// +/// Re-enables the regDMA bus clock, re-programs entry link 0 (the linked list in +/// caller memory survived in RAM) and software-triggers the restore. Must run +/// before any powered-down `TOP` peripheral is touched. No-op on chips that +/// restore in hardware. +// `#[ram]`: runs on the wake path before the flash SPI controller is restored. +#[ram] +pub(crate) fn restore_top_retention(mem: &mut SystemRetentionMemory) { + if !chip::SW_TRIGGER_REGDMA { + return; + } + regdma_clock_and_timeout(); + // The chain in `mem`/opt-in memory is intact in RAM; entry link 0 is the + // first system node, so re-point the PAU at it without rebuilding. + let head = mem.nodes.as_ptr() as u32; + fence(Ordering::SeqCst); + PAU::regs() + .regdma_link_0_addr() + .write(|w| unsafe { w.bits(head) }); + sw_trigger_system_link(false); +} + +/// Request a plain (non-CPU-retention) sleep and spin until wake or reject. In +/// deep sleep the chip resets on wake, so this never returns. +fn request_sleep_and_wait() { + let pmu = PMU::regs(); + pmu.slp_wakeup_cntl0().write(|w| w.sleep_req().bit(true)); + loop { + let int_raw = pmu.int_raw().read(); + if int_raw.soc_wakeup().bit_is_set() || int_raw.soc_sleep_reject().bit_is_set() { + break; + } + } +} + +/// Disable TIMG0's flashboot watchdog after a `TOP` power-down: TIMG0 is not +/// retained and comes back armed. +// Mirrors ESP-IDF `misc_modules_wake_prepare()`. +pub(crate) fn disable_timg0_flashboot_wdt() { + let tg0 = crate::peripherals::TIMG0::regs(); + tg0.wdtwprotect().write(|w| unsafe { w.bits(0x50D8_3AA1) }); + tg0.wdtconfig0() + .modify(|_, w| w.wdt_flashboot_mod_en().bit(false)); + tg0.wdtwprotect().write(|w| unsafe { w.bits(0) }); +} + +/// Light-sleep power-domain retention state, embedded in each chip's +/// `RtcSleepConfig`. Holds the caller's opt-in choices and retention memory plus +/// the chip-agnostic resolve/enter logic; a chip only maps the resolved decision +/// onto its own `PowerDownFlags`. +/// +/// Raw pointers (not borrows) keep the embedding `RtcSleepConfig` `Copy`; the +/// setters take `&'static mut`. +#[derive(Clone, Copy)] +pub(crate) struct SleepRetention { + cpu_power_down: bool, + top_power_down: bool, + cpu_mem: *mut CpuRetentionMemory, + top_mem: *mut SystemRetentionMemory, +} + +impl SleepRetention { + pub(crate) const fn new() -> Self { + Self { + cpu_power_down: false, + top_power_down: false, + cpu_mem: core::ptr::null_mut(), + top_mem: core::ptr::null_mut(), + } + } + + /// Opt into CPU power-down, saving CPU state into `mem`. + pub(crate) fn set_cpu_power_down(&mut self, mem: &'static mut CpuRetentionMemory) { + self.cpu_power_down = true; + self.cpu_mem = mem; + } + + /// Opt into `TOP` power-down (which also powers the CPU down), using `cpu` + /// for the CPU state and `sys` for the regDMA system-peripheral set. + pub(crate) fn set_top_power_down( + &mut self, + cpu: &'static mut CpuRetentionMemory, + sys: &'static mut SystemRetentionMemory, + ) { + self.top_power_down = true; + self.cpu_mem = cpu; + self.top_mem = sys; + } + + pub(crate) fn cpu_power_down(&self) -> bool { + self.cpu_power_down + } + + pub(crate) fn top_power_down(&self) -> bool { + self.top_power_down + } + + /// Resolve which domains may actually power down for a light sleep, given the + /// opt-in choices, caller memory and active power-domain locks. Returns + /// `(cpu_pd, top_pd)`; `top_pd` implies `cpu_pd`, and a domain only powers + /// down with its retention memory and no lock (else it clock-gates). + pub(crate) fn resolve(&self) -> (bool, bool) { + let have_cpu = !self.cpu_mem.is_null(); + let have_sys = !self.top_mem.is_null(); + let top = self.top_power_down && have_cpu && have_sys && can_power_down(Domain::Top); + let cpu = self.cpu_power_down && have_cpu && can_power_down(Domain::Cpu); + (cpu || top, top) + } + + /// Enter the sleep with the resolved `(cpu_pd, top_pd)` decision: arm TOP + /// regDMA if powering `TOP` down, then run software CPU retention (if + /// powering the CPU down) or a plain sleep request. `deep` forces the plain + /// path (retention is light-sleep only; deep sleep does not return). + /// + /// # Safety + /// + /// The PMU must already be configured for this sleep and the retention + /// memory must stay valid across it. + pub(crate) unsafe fn enter(&self, deep: bool, cpu_pd: bool, top_pd: bool) { + if !deep && top_pd && !self.top_mem.is_null() { + // After the PMU power config (which resets the backup-enable bits) + // and before the sleep request. + enable_top_retention(unsafe { &mut *self.top_mem }); + } + if !deep && cpu_pd && !self.cpu_mem.is_null() { + // Save CPU state, sleep, resume with it restored. `top_mem` is passed + // only when TOP is powered down so its (software) restore runs in RAM + // on wake; a no-op on hardware-restore chips. + let top = if top_pd { + self.top_mem + } else { + core::ptr::null_mut() + }; + unsafe { + crate::rtc_cntl::cpu_retention::sleep_with_cpu_retention(&mut *self.cpu_mem, top) + }; + } else { + request_sleep_and_wait(); + } + } +} + +/// Chip-agnostic interpreter that expands a chip's `OPS` program into PAU regDMA +/// nodes (in retention-priority order, system clock first). +// Mirrors ESP-IDF's `SLEEP_RETENTION_MODULE_SYS_PERIPH` + `..._CLOCK_SYSTEM` +// (`soc//system_retention_periph.c`, `.../sleep_clock.c`). mod sys_periph { use super::{ HEAD_LENGTH_MASK, RegdmaLink, + SYSTIMER_NODE_COUNT, + SysOp, UART_NODE_COUNT, UART_RETENTION_REGS_CNT, + build_systimer_seq, build_uart_seq, + chip, }; - /// TEE mode-control register, rewritten early on restore to unlock access. - const TEE_M4_MODE_CTRL_REG: u32 = 0x6009_8010; + /// Nodes this chip's op list expands to (sizes [`super::SystemRetentionMemory`]). + pub(super) const NODE_COUNT: usize = super::ops_node_count(chip::OPS); - /// A run of `count` consecutive 32-bit registers starting at `base`. - struct ContRegion { - base: u32, - count: u32, - } - - /// Continuous register regions to retain, in retention-priority order. The - /// sizing end register is noted per region; `count = ((end - base) / 4) + 1`. - const CONT_REGIONS: &[ContRegion] = &[ - // PRI_0 - system clock/reset (PCR) - ContRegion { - base: 0x6009_6000, - count: 79, - }, // PCR base ..= PCR_SRAM_POWER_CONF_REG (+0x138) - ContRegion { - base: 0x6009_6FF0, - count: 1, - }, // PCR_RESET_EVENT_BYPASS_REG - // PRI_4 - TEE/APM - ContRegion { - base: 0x6009_9000, - count: 68, - }, // HP_APM base ..= HP_APM_CLOCK_GATE_REG (+0x10c) - ContRegion { - base: 0x6009_8000, - count: 33, - }, // TEE base ..= TEE_CLOCK_GATE_REG (+0x80) - // PRI_5 - interrupt matrix + HP system - ContRegion { - base: 0x6001_0000, - count: 81, - }, // INTMTX base ..= INTMTX_CORE0_CLOCK_GATE_REG (+0x140) - ContRegion { - base: 0x6009_5000, - count: 18, - }, // HP_SYSTEM base ..= HP_SYSTEM_MEM_TEST_CONF_REG (+0x44) - // PRI_6 - IO MUX + GPIO matrix - ContRegion { - base: 0x6009_0000, - count: 32, - }, // IO_MUX base ..= IO_MUX_GPIO30_REG (+0x7c) - ContRegion { - base: 0x6009_1554, - count: 35, - }, // GPIO_FUNC0_OUT_SEL ..= GPIO_FUNC34_OUT_SEL - ContRegion { - base: 0x6009_114C, - count: 127, - }, // GPIO_STATUS_NEXT ..= GPIO_FUNC124_IN_SEL - ContRegion { - base: 0x6009_1000, - count: 64, - }, // GPIO base ..= GPIO_PIN34_REG (+0xfc) - // PRI_6 - Flash SPI mem (SPIMEM1 then SPIMEM0). MMU content/index - // registers are intentionally excluded (see ESP-IDF note). - ContRegion { - base: 0x6000_3000, - count: 55, - }, // SPIMEM1 base ..= SPI_MEM_SPI_SMEM_DDR (+0xd8) - ContRegion { - base: 0x6000_3100, - count: 41, - }, // SPIMEM1 FMEM_PMS0_ATTR ..= SMEM_AC (+0x1a0) - ContRegion { - base: 0x6000_3200, - count: 1, - }, // SPIMEM1 CLOCK_GATE - ContRegion { - base: 0x6000_3384, - count: 31, - }, // SPIMEM1 MMU_POWER_CTRL ..= DATE (+0x3fc) - ContRegion { - base: 0x6000_2000, - count: 55, - }, // SPIMEM0 base ..= SPI_MEM_SPI_SMEM_DDR - ContRegion { - base: 0x6000_2100, - count: 41, - }, // SPIMEM0 FMEM_PMS0_ATTR ..= SMEM_AC - ContRegion { - base: 0x6000_2200, - count: 1, - }, // SPIMEM0 CLOCK_GATE - ContRegion { - base: 0x6000_2384, - count: 31, - }, // SPIMEM0 MMU_POWER_CTRL ..= DATE - ]; - - /// [`CONT_REGIONS`] index where TEE/APM (PRI_4) starts; the PRI_2 TEE WRITE - /// node is inserted just before it. - const TEE_APM_START: usize = 2; - - /// [`CONT_REGIONS`] index where IO MUX/GPIO (PRI_6) starts; the console-UART - /// (PRI_5) nodes are inserted just before it. - const IOMUX_START: usize = 6; - - const fn total_words() -> usize { - let mut words = 0; - let mut i = 0; - while i < CONT_REGIONS.len() { - words += CONT_REGIONS[i].count as usize; - i += 1; - } - words - } - - /// Console UART0 base, retained automatically via [`build_uart_seq`]. - const UART0_BASE: u32 = 0x6000_0000; - - // SysTimer. Offsets/masks from ESP-IDF v5.4 `systimer_reg.h`; sequence from - // `systimer_regs_retention[]`. - const ST_BASE: u32 = 0x6000_A000; - const ST_CONF: u32 = ST_BASE; // +0x00 - const ST_UNIT0_OP: u32 = ST_BASE + 0x04; - const ST_UNIT1_OP: u32 = ST_BASE + 0x08; - const ST_UNIT0_LOAD_HI: u32 = ST_BASE + 0x0C; - const ST_UNIT1_LOAD_HI: u32 = ST_BASE + 0x14; - const ST_TARGET0_HI: u32 = ST_BASE + 0x1C; - const ST_TARGET0_CONF: u32 = ST_BASE + 0x34; - const ST_TARGET1_CONF: u32 = ST_BASE + 0x38; - const ST_TARGET2_CONF: u32 = ST_BASE + 0x3C; - const ST_UNIT0_VALUE_HI: u32 = ST_BASE + 0x40; - const ST_UNIT1_VALUE_HI: u32 = ST_BASE + 0x48; - const ST_COMP0_LOAD: u32 = ST_BASE + 0x50; - const ST_COMP1_LOAD: u32 = ST_BASE + 0x54; - const ST_COMP2_LOAD: u32 = ST_BASE + 0x58; - const ST_UNIT0_LOAD: u32 = ST_BASE + 0x5C; - const ST_UNIT1_LOAD: u32 = ST_BASE + 0x60; - const ST_INT_ENA: u32 = ST_BASE + 0x64; - const ST_UNIT_UPDATE: u32 = 1 << 30; - const ST_UNIT_VALUE_VALID: u32 = 1 << 29; - const ST_UNIT_LOAD: u32 = 1 << 0; - const ST_COMP_LOAD: u32 = 1 << 0; - const ST_TARGET_PERIOD_MODE: u32 = 1 << 30; - /// TARGET0_HI ..= TARGET2_CONF, i.e. all three targets' hi/lo/conf. - const ST_TARGETS_LEN: u32 = 9; - - const SYSTIMER_NODE_COUNT: usize = 19; - /// SysTimer CONTINUOUS words: unit0/1 value (2+2), targets (9), conf, int_ena. - const SYSTIMER_CONT_WORDS: usize = 2 + 2 + ST_TARGETS_LEN as usize + 1 + 1; - - /// One node per continuous region + TEE WRITE + console UART + SysTimer. - pub(super) const NODE_COUNT: usize = - CONT_REGIONS.len() + 1 + UART_NODE_COUNT + SYSTIMER_NODE_COUNT; - pub(super) const BUF_WORDS: usize = - total_words() + UART_RETENTION_REGS_CNT as usize + SYSTIMER_CONT_WORDS; - - // Every region count must fit the 10-bit node `length` field. + // Every CONTINUOUS region count must fit the 10-bit node `length` field. const _: () = { let mut i = 0; - while i < CONT_REGIONS.len() { - core::assert!(CONT_REGIONS[i].count <= HEAD_LENGTH_MASK); + while i < chip::OPS.len() { + if let SysOp::Continuous { count, .. } | SysOp::ContinuousSplit { count, .. } = + chip::OPS[i] + { + core::assert!(count <= HEAD_LENGTH_MASK); + } i += 1; } }; - /// (Re)build the SYS_PERIPH retention list into `nodes`/`buf` and return the - /// filled node slice. + /// (Re)build the SYS_PERIPH retention list into `nodes`/`buf` by walking the + /// chip's `OPS` program, and return the filled node slice. pub(super) fn build_link<'a>( nodes: &'a mut [RegdmaLink; NODE_COUNT], - buf: &mut [u32; BUF_WORDS], + buf: &mut [u32], ) -> &'a mut [RegdmaLink] { let buf_base = buf.as_mut_ptr(); let mut node = 0; let mut word = 0; - // PRI_0: system clock (PCR). - for region in &CONT_REGIONS[..TEE_APM_START] { - let mem = unsafe { buf_base.add(word) } as u32; - nodes[node] = RegdmaLink::continuous(region.base, mem, region.count); - word += region.count as usize; - node += 1; - } - - // PRI_2: restore-only WRITE clearing TEE_M4_MODE_CTRL so the TEE/APM - // restore can write freely. - nodes[node] = RegdmaLink::write(TEE_M4_MODE_CTRL_REG, 0, 0xFFFF_FFFF, true, false); - node += 1; - - // PRI_4/5: TEE/APM, interrupt matrix, HP system. - for region in &CONT_REGIONS[TEE_APM_START..IOMUX_START] { - let mem = unsafe { buf_base.add(word) } as u32; - nodes[node] = RegdmaLink::continuous(region.base, mem, region.count); - word += region.count as usize; - node += 1; - } - - // PRI_5: console UART0 (same sequence as the opt-in path). - let mem = unsafe { buf_base.add(word) } as u32; - build_uart_seq(UART0_BASE, &mut nodes[node..node + UART_NODE_COUNT], mem); - word += UART_RETENTION_REGS_CNT as usize; - node += UART_NODE_COUNT; - - // PRI_6: IO MUX, GPIO matrix, SPI mem. - for region in &CONT_REGIONS[IOMUX_START..] { - let mem = unsafe { buf_base.add(word) } as u32; - nodes[node] = RegdmaLink::continuous(region.base, mem, region.count); - word += region.count as usize; - node += 1; - } - - // PRI_6: SysTimer. Backup latches each unit's counter (UPDATE + wait for - // VALUE_VALID) and reads it; restore loads it back and triggers a load. - // The value is read from VALUE_HI/LO but restored into LOAD_HI/LO, hence - // the split backup/restore addresses. - let alloc = |len: u32, word: &mut usize| -> u32 { - let mem = unsafe { buf_base.add(*word) } as u32; - *word += len as usize; - mem - }; - - // Unit 0: latch + read value, restore into load. - nodes[node] = RegdmaLink::write(ST_UNIT0_OP, ST_UNIT_UPDATE, ST_UNIT_UPDATE, false, true); - node += 1; - nodes[node] = RegdmaLink::wait( - ST_UNIT0_OP, - ST_UNIT_VALUE_VALID, - ST_UNIT_VALUE_VALID, - false, - true, - ); - node += 1; - let mem = alloc(2, &mut word); - nodes[node] = RegdmaLink::continuous_split(ST_UNIT0_VALUE_HI, ST_UNIT0_LOAD_HI, mem, 2); - node += 1; - nodes[node] = RegdmaLink::write(ST_UNIT0_LOAD, ST_UNIT_LOAD, ST_UNIT_LOAD, true, false); - node += 1; - - // Unit 1. - nodes[node] = RegdmaLink::write(ST_UNIT1_OP, ST_UNIT_UPDATE, ST_UNIT_UPDATE, false, true); - node += 1; - nodes[node] = RegdmaLink::wait( - ST_UNIT1_OP, - ST_UNIT_VALUE_VALID, - ST_UNIT_VALUE_VALID, - false, - true, - ); - node += 1; - let mem = alloc(2, &mut word); - nodes[node] = RegdmaLink::continuous_split(ST_UNIT1_VALUE_HI, ST_UNIT1_LOAD_HI, mem, 2); - node += 1; - nodes[node] = RegdmaLink::write(ST_UNIT1_LOAD, ST_UNIT_LOAD, ST_UNIT_LOAD, true, false); - node += 1; - - // Comparator target values & periods. - let mem = alloc(ST_TARGETS_LEN, &mut word); - nodes[node] = RegdmaLink::continuous(ST_TARGET0_HI, mem, ST_TARGETS_LEN); - node += 1; - for comp in [ST_COMP0_LOAD, ST_COMP1_LOAD, ST_COMP2_LOAD] { - nodes[node] = RegdmaLink::write(comp, ST_COMP_LOAD, ST_COMP_LOAD, true, false); - node += 1; - } - // Re-arm period mode: clear+set for target0/1, clear for target2. - for target in [ST_TARGET0_CONF, ST_TARGET1_CONF] { - nodes[node] = RegdmaLink::write(target, 0, ST_TARGET_PERIOD_MODE, true, false); - node += 1; - nodes[node] = RegdmaLink::write( - target, - ST_TARGET_PERIOD_MODE, - ST_TARGET_PERIOD_MODE, - true, - false, - ); - node += 1; + for op in chip::OPS { + match *op { + SysOp::Continuous { base, count } => { + let mem = unsafe { buf_base.add(word) } as u32; + nodes[node] = RegdmaLink::continuous(base, mem, count); + word += count as usize; + node += 1; + } + SysOp::ContinuousSplit { + backup, + restore, + count, + } => { + let mem = unsafe { buf_base.add(word) } as u32; + nodes[node] = RegdmaLink::continuous_split(backup, restore, mem, count); + word += count as usize; + node += 1; + } + SysOp::Write { addr, value, mask } => { + nodes[node] = RegdmaLink::write(addr, value, mask, true, false); + node += 1; + } + SysOp::Wait { addr, value, mask } => { + nodes[node] = RegdmaLink::wait(addr, value, mask, true, false); + node += 1; + } + SysOp::Uart { base } => { + let mem = unsafe { buf_base.add(word) } as u32; + build_uart_seq(base, &mut nodes[node..node + UART_NODE_COUNT], mem); + word += UART_RETENTION_REGS_CNT as usize; + node += UART_NODE_COUNT; + } + SysOp::Systimer { base } => { + build_systimer_seq( + base, + &mut nodes[node..node + SYSTIMER_NODE_COUNT], + buf_base, + word, + ); + node += SYSTIMER_NODE_COUNT; + word += super::SYSTIMER_CONT_WORDS; + } + } } - nodes[node] = RegdmaLink::write(ST_TARGET2_CONF, 0, ST_TARGET_PERIOD_MODE, true, false); - node += 1; - - // Work-enable and interrupt-enable state. - let mem = alloc(1, &mut word); - nodes[node] = RegdmaLink::continuous(ST_CONF, mem, 1); - node += 1; - let mem = alloc(1, &mut word); - nodes[node] = RegdmaLink::continuous(ST_INT_ENA, mem, 1); - node += 1; &mut nodes[..node] } diff --git a/esp-hal/src/rtc_cntl/retention/esp32c6.rs b/esp-hal/src/rtc_cntl/retention/esp32c6.rs new file mode 100644 index 00000000000..7643d9b0112 --- /dev/null +++ b/esp-hal/src/rtc_cntl/retention/esp32c6.rs @@ -0,0 +1,126 @@ +//! ESP32-C6 register data for TOP-domain regDMA retention and CPU-domain +//! software retention. +//! +//! Pure data consumed by the chip-agnostic logic in `retention` and +//! `cpu_retention`. Region sizes note the sizing end +//! register (`count = ((end - base) / 4) + 1`). + +// References (ESP-IDF `v5.4`): `soc/esp32c6/system_retention_periph.c`, +// `esp_hw_support/.../esp32c6/sleep_clock.c`, `.../esp32c6/sleep_cpu.c`. + +use super::SysOp::{self, Continuous, ContinuousSplit, Systimer, Uart, Write}; + +/// The TOP-domain SYS_PERIPH retention program, in retention-priority order +/// (system clock first). Interpreted by `retention::sys_periph::build_link`. +pub(super) const OPS: &[SysOp] = &[ + // PRI_0: system clock/reset (PCR). + Continuous { + base: 0x6009_6000, + count: 79, + }, // PCR ..= PCR_SRAM_POWER_CONF_REG (+0x138) + Continuous { + base: 0x6009_6FF0, + count: 1, + }, // PCR_RESET_EVENT_BYPASS_REG + // PRI_2: unlock TEE/APM (clear TEE_M4_MODE_CTRL) before restoring them. + Write { + addr: 0x6009_8010, + value: 0, + mask: 0xFFFF_FFFF, + }, // TEE_M4_MODE_CTRL_REG + // PRI_4/5: TEE/APM, interrupt matrix, HP system. + Continuous { + base: 0x6009_9000, + count: 68, + }, // HP_APM ..= HP_APM_CLOCK_GATE_REG (+0x10c) + Continuous { + base: 0x6009_8000, + count: 33, + }, // TEE ..= TEE_CLOCK_GATE_REG (+0x80) + Continuous { + base: 0x6001_0000, + count: 81, + }, // INTMTX ..= INTMTX_CORE0_CLOCK_GATE_REG (+0x140) + Continuous { + base: 0x6009_5000, + count: 18, + }, // HP_SYSTEM ..= HP_SYSTEM_MEM_TEST_CONF_REG (+0x44) + // PRI_5: console UART0. + Uart { base: 0x6000_0000 }, + // PRI_6: IO MUX + GPIO matrix. + Continuous { + base: 0x6009_0000, + count: 32, + }, // IO_MUX ..= IO_MUX_GPIO30_REG (+0x7c) + Continuous { + base: 0x6009_1554, + count: 35, + }, // GPIO_FUNC0_OUT_SEL ..= GPIO_FUNC34_OUT_SEL + Continuous { + base: 0x6009_114C, + count: 127, + }, // GPIO_STATUS_NEXT ..= GPIO_FUNC124_IN_SEL + Continuous { + base: 0x6009_1000, + count: 64, + }, // GPIO ..= GPIO_PIN34_REG (+0xfc) + ContinuousSplit { + backup: 0x6009_1020, + restore: 0x6009_1024, + count: 1, + }, // GPIO_ENABLE_REG .. GPIO_ENABLE_W1TS_REG + ContinuousSplit { + backup: 0x6009_102c, + restore: 0x6009_1030, + count: 1, + }, // GPIO_ENABLE1_REG .. GPIO_ENABLE1_W1TS_REG (pins 32..=34) + // PRI_6: Flash SPI mem (SPIMEM1 then SPIMEM0). MMU content/index registers + // are intentionally excluded (see ESP-IDF note). + Continuous { + base: 0x6000_3000, + count: 55, + }, // SPIMEM1 ..= SPI_MEM_SPI_SMEM_DDR (+0xd8) + Continuous { + base: 0x6000_3100, + count: 41, + }, // SPIMEM1 FMEM_PMS0_ATTR ..= SMEM_AC (+0x1a0) + Continuous { + base: 0x6000_3200, + count: 1, + }, // SPIMEM1 CLOCK_GATE + Continuous { + base: 0x6000_3384, + count: 31, + }, // SPIMEM1 MMU_POWER_CTRL ..= DATE (+0x3fc) + Continuous { + base: 0x6000_2000, + count: 55, + }, // SPIMEM0 ..= SPI_MEM_SPI_SMEM_DDR + Continuous { + base: 0x6000_2100, + count: 41, + }, // SPIMEM0 FMEM_PMS0_ATTR ..= SMEM_AC + Continuous { + base: 0x6000_2200, + count: 1, + }, // SPIMEM0 CLOCK_GATE + Continuous { + base: 0x6000_2384, + count: 31, + }, // SPIMEM0 MMU_POWER_CTRL ..= DATE + // PRI_6: SysTimer. + Systimer { base: 0x6000_A000 }, +]; + +/// The C6's PAU survives the `TOP` power-down (it is in `HP_AON`), so the PMU +/// drives the regDMA backup/restore in hardware on the sleep/wake transition. +pub(super) const SW_TRIGGER_REGDMA: bool = false; + +// CPU-domain device-register bases lost when `pd_cpu` powers down (consumed by +// `cpu_retention`; the region layout around them is shared). +pub(crate) const INTPRI_BASE: u32 = 0x600C_5000; // interrupt priority (INTPRI) +pub(crate) const CACHE_BASE: u32 = 0x600C_8000; // L1 cache control (EXTMEM/CACHE) +pub(crate) const PLIC_MX_BASE: u32 = 0x2000_1000; // PLIC machine interrupts +pub(crate) const PLIC_UX_BASE: u32 = 0x2000_1400; // PLIC user interrupts +pub(crate) const CLINT_MINT_BASE: u32 = 0x2000_1800; // CLINT machine timer +pub(crate) const CLINT_UINT_BASE: u32 = 0x2000_1C00; // CLINT user timer diff --git a/esp-hal/src/rtc_cntl/retention/esp32h2.rs b/esp-hal/src/rtc_cntl/retention/esp32h2.rs new file mode 100644 index 00000000000..25b595f54e0 --- /dev/null +++ b/esp-hal/src/rtc_cntl/retention/esp32h2.rs @@ -0,0 +1,131 @@ +//! ESP32-H2 register data for TOP-domain regDMA retention and CPU-domain +//! software retention. +//! +//! Pure data consumed by the chip-agnostic logic in `retention` and +//! `cpu_retention`. Region sizes note the sizing end register +//! (`count = ((end - base) / 4) + 1`). + +// References (ESP-IDF `v5.4`): `soc/esp32h2/system_retention_periph.c`, +// `esp_hw_support/.../esp32h2/sleep_clock.c`, `.../esp32h2/sleep_cpu.c`. + +use super::SysOp::{self, Continuous, ContinuousSplit, Systimer, Uart, Wait, Write}; + +/// The TOP-domain SYS_PERIPH retention program, in retention-priority order +/// (system clock first). Interpreted by `retention::sys_periph::build_link`. +pub(super) const OPS: &[SysOp] = &[ + // PRI_0: system clock/reset (PCR). The H2 must also pulse the bus-clock + // update bit on restore for the new clock config to take effect. + Continuous { + base: 0x6009_6000, + count: 85, + }, // PCR ..= PCR_PWDET_SAR_CLK_CONF_REG (+0x150) + Continuous { + base: 0x6009_6FF0, + count: 1, + }, // PCR_RESET_EVENT_BYPASS_REG + Write { + addr: 0x6009_6148, + value: 0x1, + mask: 0x1, + }, // PCR_BUS_CLK_UPDATE (BUS_CLOCK_UPDATE) + Wait { + addr: 0x6009_6148, + value: 0, + mask: 0x1, + }, // wait for it to self-clear + // PRI_2: unlock TEE/APM (clear TEE_M4_MODE_CTRL) before restoring them. + Write { + addr: 0x6009_8010, + value: 0, + mask: 0xFFFF_FFFF, + }, // TEE_M4_MODE_CTRL_REG + // PRI_4/5: TEE/APM, interrupt matrix, HP system. + Continuous { + base: 0x6009_9000, + count: 68, + }, // HP_APM ..= HP_APM_CLOCK_GATE_REG (+0x10c) + Continuous { + base: 0x6009_8000, + count: 33, + }, // TEE ..= TEE_CLOCK_GATE_REG (+0x80) + Continuous { + base: 0x6001_0000, + count: 69, + }, // INTMTX ..= INTMTX_CORE0_CLOCK_GATE_REG (+0x110) + Continuous { + base: 0x6009_5000, + count: 12, + }, // HP_SYSTEM ..= HP_SYSTEM_MEM_TEST_CONF_REG (+0x2c) + // PRI_5: console UART0. + Uart { base: 0x6000_0000 }, + // PRI_6: IO MUX + GPIO matrix (fewer pins than the C6). + Continuous { + base: 0x6009_0000, + count: 29, + }, // IO_MUX ..= IO_MUX_GPIO27_REG (+0x70) + Continuous { + base: 0x6009_1554, + count: 32, + }, // GPIO_FUNC0_OUT_SEL ..= GPIO_FUNC31_OUT_SEL + Continuous { + base: 0x6009_114C, + count: 127, + }, // GPIO_STATUS_NEXT ..= GPIO_FUNC124_IN_SEL + Continuous { + base: 0x6009_1000, + count: 61, + }, // GPIO ..= GPIO_PIN31_REG (+0xf0) + ContinuousSplit { + backup: 0x6009_1020, + restore: 0x6009_1024, + count: 1, + }, // GPIO_ENABLE_REG .. GPIO_ENABLE_W1TS_REG + // PRI_6: Flash SPI mem (SPIMEM1 then SPIMEM0), identical layout to the C6. + // MMU content/index registers are intentionally excluded (see ESP-IDF note). + Continuous { + base: 0x6000_3000, + count: 55, + }, // SPIMEM1 ..= SPI_MEM_SPI_SMEM_DDR (+0xd8) + Continuous { + base: 0x6000_3100, + count: 41, + }, // SPIMEM1 FMEM_PMS0_ATTR ..= SMEM_AC (+0x1a0) + Continuous { + base: 0x6000_3200, + count: 1, + }, // SPIMEM1 CLOCK_GATE + Continuous { + base: 0x6000_3384, + count: 31, + }, // SPIMEM1 MMU_POWER_CTRL ..= DATE (+0x3fc) + Continuous { + base: 0x6000_2000, + count: 55, + }, // SPIMEM0 ..= SPI_MEM_SPI_SMEM_DDR + Continuous { + base: 0x6000_2100, + count: 41, + }, // SPIMEM0 FMEM_PMS0_ATTR ..= SMEM_AC + Continuous { + base: 0x6000_2200, + count: 1, + }, // SPIMEM0 CLOCK_GATE + Continuous { + base: 0x6000_2384, + count: 31, + }, // SPIMEM0 MMU_POWER_CTRL ..= DATE + // PRI_6: SysTimer (base differs from the C6). + Systimer { base: 0x6000_B000 }, +]; + +// ESP32-H2 use software to trigger REGDMA to restore instead of PMU, because regdma has power bug. +pub(super) const SW_TRIGGER_REGDMA: bool = true; + +// CPU-domain device-register bases lost when `pd_cpu` powers down (consumed by +// `cpu_retention`). Identical to the C6 (same RISC-V core/cache/PLIC/CLINT). +pub(crate) const INTPRI_BASE: u32 = 0x600C_5000; // interrupt priority (INTPRI) +pub(crate) const CACHE_BASE: u32 = 0x600C_8000; // L1 cache control (CACHE) +pub(crate) const PLIC_MX_BASE: u32 = 0x2000_1000; // PLIC machine interrupts +pub(crate) const PLIC_UX_BASE: u32 = 0x2000_1400; // PLIC user interrupts +pub(crate) const CLINT_MINT_BASE: u32 = 0x2000_1800; // CLINT machine timer +pub(crate) const CLINT_UINT_BASE: u32 = 0x2000_1C00; // CLINT user timer diff --git a/esp-hal/src/rtc_cntl/sleep/esp32c6.rs b/esp-hal/src/rtc_cntl/sleep/esp32c6.rs index 975cbdcfcbc..a7d2d880ecc 100644 --- a/esp-hal/src/rtc_cntl/sleep/esp32c6.rs +++ b/esp-hal/src/rtc_cntl/sleep/esp32c6.rs @@ -571,15 +571,8 @@ pub struct RtcSleepConfig { /// Power Down flags. On the C6 `apply()` sets the `pd_cpu`/`pd_top` bits, so /// a domain can't power off without the caller's retention storage. pub(crate) pd_flags: PowerDownFlags, - /// See [`Self::with_cpu_power_down`]. - cpu_power_down: bool, - /// See [`Self::with_top_power_down`]. - top_power_down: bool, - /// CPU-domain retention store; null when only clock-gating. A raw pointer - /// (not a borrow) keeps `Self` `Copy`; the builders take a `&'static mut`. - cpu_retention_mem: *mut crate::rtc_cntl::cpu_retention::CpuRetentionMemory, - /// TOP-domain system-peripheral regDMA store; null when not opted in. - top_retention_mem: *mut crate::rtc_cntl::retention::SystemRetentionMemory, + /// Light-sleep CPU/TOP power-down retention (opt-in choices + caller memory). + retention: crate::rtc_cntl::retention::SleepRetention, } impl Default for RtcSleepConfig { @@ -590,10 +583,7 @@ impl Default for RtcSleepConfig { Self { deep: false, pd_flags: PowerDownFlags(0), - cpu_power_down: false, - top_power_down: false, - cpu_retention_mem: core::ptr::null_mut(), - top_retention_mem: core::ptr::null_mut(), + retention: crate::rtc_cntl::retention::SleepRetention::new(), } } } @@ -614,15 +604,14 @@ impl RtcSleepConfig { mut self, memory: &'static mut crate::rtc_cntl::cpu_retention::CpuRetentionMemory, ) -> Self { - self.cpu_power_down = true; - self.cpu_retention_mem = memory; + self.retention.set_cpu_power_down(memory); self } /// Returns whether the CPU power domain is powered down during light sleep. #[instability::unstable] pub fn cpu_power_down(&self) -> bool { - self.cpu_power_down + self.retention.cpu_power_down() } /// Power down the digital `TOP` power domain during light sleep. @@ -641,16 +630,14 @@ impl RtcSleepConfig { cpu_memory: &'static mut crate::rtc_cntl::cpu_retention::CpuRetentionMemory, system_memory: &'static mut crate::rtc_cntl::retention::SystemRetentionMemory, ) -> Self { - self.top_power_down = true; - self.cpu_retention_mem = cpu_memory; - self.top_retention_mem = system_memory; + self.retention.set_top_power_down(cpu_memory, system_memory); self } /// Returns whether the TOP power domain is powered down during light sleep. #[instability::unstable] pub fn top_power_down(&self) -> bool { - self.top_power_down + self.retention.top_power_down() } } @@ -804,21 +791,11 @@ impl RtcSleepConfig { self.pd_flags.set_pd_rc_fast(true); self.pd_flags.set_pd_xtal32k(!lp_slow_uses_xtal32k); - // Only power a domain down with the caller's retention storage and no - // active power-domain lock on it; otherwise fall back to clock-gating. - use crate::rtc_cntl::power_domain::{Domain, can_power_down}; - let have_cpu_mem = !self.cpu_retention_mem.is_null(); - let have_sys_mem = !self.top_retention_mem.is_null(); - - // TOP-pd needs both the CPU frame buffer (it implies CPU-pd) and the - // system-peripheral regDMA buffer. - let top_pd = - self.top_power_down && have_cpu_mem && have_sys_mem && can_power_down(Domain::Top); - let cpu_pd = self.cpu_power_down && have_cpu_mem && can_power_down(Domain::Cpu); - - // The CPU can't survive a TOP power-down, so pd_top implies pd_cpu. + // A domain only powers down with the caller's retention storage and + // no active lock (else clock-gating); pd_top implies pd_cpu. + let (cpu_pd, top_pd) = self.retention.resolve(); self.pd_flags.set_pd_top(top_pd); - self.pd_flags.set_pd_cpu(cpu_pd || top_pd); + self.pd_flags.set_pd_cpu(cpu_pd); } } @@ -908,44 +885,15 @@ impl RtcSleepConfig { // Start entry into sleep mode - // Arm regDMA TOP-domain retention: after the power config write above - // (which resets the backup-enable bits) and before the sleep request. - if !self.deep && self.pd_flags.pd_top() && !self.top_retention_mem.is_null() { - let system_memory = unsafe { &mut *self.top_retention_mem }; - crate::rtc_cntl::retention::enable_top_retention(system_memory); - } - - if !self.deep && self.pd_flags.pd_cpu() && !self.cpu_retention_mem.is_null() { - // CPU power-down light sleep: save state, sleep, resume with it - // restored. Non-null whenever apply() sets pd_cpu. - let memory = unsafe { &mut *self.cpu_retention_mem }; - unsafe { - crate::rtc_cntl::cpu_retention::sleep_with_cpu_retention(memory); - } - } else { - // pmu_ll_hp_set_sleep_enable - PMU::regs() - .slp_wakeup_cntl0() - .write(|w| w.sleep_req().bit(true)); - - // In pd_cpu lightsleep and deepsleep mode, we never get here - loop { - let int_raw = PMU::regs().int_raw().read(); - if int_raw.soc_wakeup().bit_is_set() || int_raw.soc_sleep_reject().bit_is_set() { - break; - } - } + // Arm retention (if any) and enter sleep. Arming must run after the + // power config above, which resets the regDMA backup-enable bits. + unsafe { + self.retention + .enter(self.deep, self.pd_flags.pd_cpu(), self.pd_flags.pd_top()); } - // After a TOP power-down TIMG0 (not retained) comes back with its - // flashboot watchdog armed; disable it (like IDF's - // misc_modules_wake_prepare()) or it soon resets the chip. if !self.deep && self.pd_flags.pd_top() { - let tg0 = crate::peripherals::TIMG0::regs(); - tg0.wdtwprotect().write(|w| unsafe { w.bits(0x50D8_3AA1) }); - tg0.wdtconfig0() - .modify(|_, w| w.wdt_flashboot_mod_en().bit(false)); - tg0.wdtwprotect().write(|w| unsafe { w.bits(0) }); + crate::rtc_cntl::retention::disable_timg0_flashboot_wdt(); } } diff --git a/esp-hal/src/rtc_cntl/sleep/esp32h2.rs b/esp-hal/src/rtc_cntl/sleep/esp32h2.rs index 2008bfc8c04..26cc691793a 100644 --- a/esp-hal/src/rtc_cntl/sleep/esp32h2.rs +++ b/esp-hal/src/rtc_cntl/sleep/esp32h2.rs @@ -409,6 +409,8 @@ pub struct RtcSleepConfig { pub pd_flags: PowerDownFlags, /// Indicates whether the top power domain should remain enabled during sleep. need_pd_top: bool, + /// Light-sleep CPU/TOP power-down retention (opt-in choices + caller memory). + retention: crate::rtc_cntl::retention::SleepRetention, } impl Default for RtcSleepConfig { @@ -420,10 +422,64 @@ impl Default for RtcSleepConfig { deep: false, pd_flags: PowerDownFlags(0), need_pd_top: false, + retention: crate::rtc_cntl::retention::SleepRetention::new(), } } } +impl RtcSleepConfig { + /// Power down the CPU power domain during light sleep. + /// + /// CPU state is saved/restored in software into the caller's + /// [`CpuRetentionMemory`]. No effect on deep sleep. See + /// [`cpu_retention::cpu_power_down_wake_count`] to confirm the domain lost + /// power. + /// + /// [`CpuRetentionMemory`]: crate::rtc_cntl::cpu_retention::CpuRetentionMemory + /// [`cpu_retention::cpu_power_down_wake_count`]: crate::rtc_cntl::cpu_retention::cpu_power_down_wake_count + #[instability::unstable] + #[must_use] + pub fn with_cpu_power_down( + mut self, + memory: &'static mut crate::rtc_cntl::cpu_retention::CpuRetentionMemory, + ) -> Self { + self.retention.set_cpu_power_down(memory); + self + } + + /// Returns whether the CPU power domain is powered down during light sleep. + #[instability::unstable] + pub fn cpu_power_down(&self) -> bool { + self.retention.cpu_power_down() + } + + /// Power down the digital `TOP` power domain during light sleep. + /// + /// Core system peripherals are backed up to the caller's + /// [`SystemRetentionMemory`] by regDMA and restored on wakeup. This also + /// powers the CPU down, so it needs a [`CpuRetentionMemory`] too. No effect + /// on deep sleep. + /// + /// [`CpuRetentionMemory`]: crate::rtc_cntl::cpu_retention::CpuRetentionMemory + /// [`SystemRetentionMemory`]: crate::rtc_cntl::cpu_retention::SystemRetentionMemory + #[instability::unstable] + #[must_use] + pub fn with_top_power_down( + mut self, + cpu_memory: &'static mut crate::rtc_cntl::cpu_retention::CpuRetentionMemory, + system_memory: &'static mut crate::rtc_cntl::retention::SystemRetentionMemory, + ) -> Self { + self.retention.set_top_power_down(cpu_memory, system_memory); + self + } + + /// Returns whether the TOP power domain is powered down during light sleep. + #[instability::unstable] + pub fn top_power_down(&self) -> bool { + self.retention.top_power_down() + } +} + bitfield::bitfield! { #[derive(Clone, Copy)] /// Power domains to be powered down during sleep @@ -511,15 +567,22 @@ impl RtcSleepConfig { self.pd_flags.set_pd_rc_fast(true); self.pd_flags.set_pd_xtal32k(!lp_slow_uses_xtal32k); } else { - // Light sleep: the digital and top domains stay powered (so execution - // resumes in place; the top domain must also stay on for any EXT1/GPIO - // wakeup). Power down the analog clock sources nothing needs while the + // Light sleep: the digital domain (CPU, RAM, peripherals) stays + // powered and only clock-gated by default, so execution resumes in + // place. Power down the analog clock sources nothing needs while the // core is clock-gated to cut power. Unlike the C5/C6 family, H2's - // light-sleep analog config does not lower the HP voltage, so this saves - // the oscillator current but not regulator power. + // light-sleep analog config does not lower the HP voltage, so this + // saves the oscillator current but not regulator power. self.pd_flags.set_pd_xtal(true); self.pd_flags.set_pd_rc_fast(true); self.pd_flags.set_pd_xtal32k(!lp_slow_uses_xtal32k); + + // A domain only powers down with the caller's retention storage and + // no active lock (else clock-gating); pd_top implies pd_cpu. Shares + // the chip-agnostic regDMA/CPU-retention path with the C6. + let (cpu_pd, top_pd) = self.retention.resolve(); + self.pd_flags.set_pd_top(top_pd); + self.pd_flags.set_pd_cpu(cpu_pd); } } @@ -606,18 +669,15 @@ impl RtcSleepConfig { .slp_wakeup_cntl4() .write(|w| w.slp_reject_cause_clr().bit(true)); - // Start entry into sleep mode + // Arm retention (if any) and enter sleep. Arming must run after the + // power config above, which resets the regDMA backup-enable bits. + unsafe { + self.retention + .enter(self.deep, self.pd_flags.pd_cpu(), self.pd_flags.pd_top()); + } - // pmu_ll_hp_set_sleep_enable - PMU::regs() - .slp_wakeup_cntl0() - .write(|w| w.sleep_req().bit(true)); - - loop { - let int_raw = PMU::regs().int_raw().read(); - if int_raw.soc_wakeup().bit_is_set() || int_raw.soc_sleep_reject().bit_is_set() { - break; - } + if !self.deep && self.pd_flags.pd_top() { + crate::rtc_cntl::retention::disable_timg0_flashboot_wdt(); } } diff --git a/esp-hal/src/spi/master/low_level/mod.rs b/esp-hal/src/spi/master/low_level/mod.rs index 9da4a5037a0..a281f47675e 100644 --- a/esp-hal/src/spi/master/low_level/mod.rs +++ b/esp-hal/src/spi/master/low_level/mod.rs @@ -48,7 +48,7 @@ pub(super) struct SpiWrapper<'d> { pub(super) spi: AnySpi<'d>, _guard: PeripheralGuard, // Active keeps `TOP` powered; `with_retention_memory` swaps to retained. - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] pub(super) power: crate::rtc_cntl::retention::PowerManagement< 'd, crate::rtc_cntl::retention::SpiRetentionMemory, @@ -57,7 +57,7 @@ pub(super) struct SpiWrapper<'d> { // registers stay accessible to regDMA at TOP power-down/restore. The driver // otherwise only enables it transiently around config writes, leaving it // gated at sleep entry, which makes regDMA back up/restore zeros. - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] pub(super) _retention_clock: Option, } @@ -67,9 +67,9 @@ impl<'d> SpiWrapper<'d> { let this = Self { spi: spi.degrade(), _guard: PeripheralGuard::new(p), - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] power: crate::rtc_cntl::retention::PowerManagement::new(), - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] _retention_clock: None, }; diff --git a/esp-hal/src/spi/master/mod.rs b/esp-hal/src/spi/master/mod.rs index ba101ad8c70..26ae69ba2f0 100644 --- a/esp-hal/src/spi/master/mod.rs +++ b/esp-hal/src/spi/master/mod.rs @@ -51,7 +51,7 @@ pub use low_level::{Info, Instance, QspiInstance, State}; use procmacros::doc_replace; use super::{BitOrder, Error, Mode}; -#[cfg(esp32c6)] +#[cfg(sleep_pd_retention)] #[instability::unstable] pub use crate::rtc_cntl::retention::SpiRetentionMemory; use crate::{ @@ -997,7 +997,7 @@ where { /// Retain this SPI's config registers in `mem` across a `TOP` power-down in /// light sleep, dropping the lock that would otherwise keep `TOP` powered. - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] #[instability::unstable] pub fn with_retention_memory(mut self, mem: &'d mut SpiRetentionMemory) -> Self { let base = self.driver().regs() as *const _ as usize as u32; diff --git a/esp-hal/src/uart/mod.rs b/esp-hal/src/uart/mod.rs index 3fb5797ebf7..6d339ebe0bc 100644 --- a/esp-hal/src/uart/mod.rs +++ b/esp-hal/src/uart/mod.rs @@ -75,9 +75,9 @@ use low_level::{ sync_regs, }; -#[cfg(not(esp32c6))] +#[cfg(not(sleep_pd_retention))] use crate::rtc_cntl::WakeLock; -#[cfg(esp32c6)] +#[cfg(sleep_pd_retention)] #[instability::unstable] pub use crate::rtc_cntl::retention::UartRetentionMemory; use crate::{ @@ -94,7 +94,6 @@ use crate::{ interrupt::InterruptHandler, pac::uart0::RegisterBlock, private::DropGuard, - rtc_cntl::WakeLock, system::PeripheralGuard, }; @@ -556,9 +555,9 @@ where guard: rx_guard, peri_clock_guard: peri_clock_guard.clone(), // Receiving data continuously, the peripheral can't let the system sleep. - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] power: crate::rtc_cntl::retention::PowerManagement::new(), - #[cfg(not(esp32c6))] + #[cfg(not(sleep_pd_retention))] _wake_lock: WakeLock::new(), reported_errors: config.rx.reported_errors, }, @@ -618,10 +617,10 @@ pub struct UartRx<'d, Dm: DriverMode> { guard: PeripheralGuard, peri_clock_guard: UartClockGuard<'d>, // Active keeps `TOP` powered; `with_retention_memory` swaps to retained. - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] power: crate::rtc_cntl::retention::PowerManagement<'d, UartRetentionMemory>, // Receiving data continuously, the peripheral can't let the system sleep. - #[cfg(not(esp32c6))] + #[cfg(not(sleep_pd_retention))] _wake_lock: WakeLock, reported_errors: EnumSet, } @@ -1087,9 +1086,9 @@ impl<'d> UartRx<'d, Blocking> { phantom: PhantomData, guard: self.guard, peri_clock_guard: self.peri_clock_guard, - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] power: self.power, - #[cfg(not(esp32c6))] + #[cfg(not(sleep_pd_retention))] _wake_lock: self._wake_lock, reported_errors: self.reported_errors, } @@ -1113,9 +1112,9 @@ impl<'d> UartRx<'d, Async> { phantom: PhantomData, guard: self.guard, peri_clock_guard: self.peri_clock_guard, - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] power: self.power, - #[cfg(not(esp32c6))] + #[cfg(not(sleep_pd_retention))] _wake_lock: self._wake_lock, reported_errors: self.reported_errors, } @@ -1870,7 +1869,7 @@ where /// Retain this UART's config registers in `mem` across a `TOP` power-down in /// light sleep, dropping the lock that would otherwise keep `TOP` powered. /// The console/log UART is retained automatically and does not need this. - #[cfg(esp32c6)] + #[cfg(sleep_pd_retention)] #[instability::unstable] pub fn with_retention_memory(mut self, mem: &'d mut UartRetentionMemory) -> Self { let base = self.regs() as *const RegisterBlock as usize as u32; diff --git a/esp-metadata-generated/src/_build_script_utils.rs b/esp-metadata-generated/src/_build_script_utils.rs index 247cd02e268..5080ddf9839 100644 --- a/esp-metadata-generated/src/_build_script_utils.rs +++ b/esp-metadata-generated/src/_build_script_utils.rs @@ -2950,6 +2950,7 @@ impl Chip { "sleep_has_wakeup_source_sdio", "sleep_has_wakeup_source_bt", "sleep_has_wakeup_source_lp_core", + "sleep_pd_retention", "soc_cpu_has_csr_pc", "soc_cpu_csr_prv_mode=\"3088\"", "soc_cpu_csr_prv_mode_is_set", @@ -3260,6 +3261,7 @@ impl Chip { "cargo:rustc-cfg=sleep_has_wakeup_source_sdio", "cargo:rustc-cfg=sleep_has_wakeup_source_bt", "cargo:rustc-cfg=sleep_has_wakeup_source_lp_core", + "cargo:rustc-cfg=sleep_pd_retention", "cargo:rustc-cfg=soc_cpu_has_csr_pc", "cargo:rustc-cfg=soc_cpu_csr_prv_mode=\"3088\"", "cargo:rustc-cfg=soc_cpu_csr_prv_mode_is_set", @@ -4331,6 +4333,7 @@ impl Chip { "sleep_has_wakeup_source_sdio", "sleep_has_wakeup_source_bt", "sleep_has_wakeup_source_lp_core", + "sleep_pd_retention", "soc_cpu_has_csr_pc", "soc_cpu_csr_prv_mode=\"3088\"", "soc_cpu_csr_prv_mode_is_set", @@ -4610,6 +4613,7 @@ impl Chip { "cargo:rustc-cfg=sleep_has_wakeup_source_sdio", "cargo:rustc-cfg=sleep_has_wakeup_source_bt", "cargo:rustc-cfg=sleep_has_wakeup_source_lp_core", + "cargo:rustc-cfg=sleep_pd_retention", "cargo:rustc-cfg=soc_cpu_has_csr_pc", "cargo:rustc-cfg=soc_cpu_csr_prv_mode=\"3088\"", "cargo:rustc-cfg=soc_cpu_csr_prv_mode_is_set", @@ -8161,6 +8165,7 @@ impl Chip { "lp_uart_driver_supported", "ulp_riscv_driver_supported", "lp_i2c_master_fifo_size_is_set", + "sleep_pd_retention", "soc_has_clock_node_soc_root_clk", "soc_has_clock_node_cpu_hs_div", "soc_has_clock_node_cpu_ls_div", diff --git a/esp-metadata-generated/src/_generated_esp32.rs b/esp-metadata-generated/src/_generated_esp32.rs index 07fa67b2742..480965cedfb 100644 --- a/esp-metadata-generated/src/_generated_esp32.rs +++ b/esp-metadata-generated/src/_generated_esp32.rs @@ -376,6 +376,9 @@ macro_rules! property { ("sleep.deep_sleep") => { true }; + ("sleep.pd_retention") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c2.rs b/esp-metadata-generated/src/_generated_esp32c2.rs index 9caf18e3bf4..fdc77c21d2c 100644 --- a/esp-metadata-generated/src/_generated_esp32c2.rs +++ b/esp-metadata-generated/src/_generated_esp32c2.rs @@ -283,6 +283,9 @@ macro_rules! property { ("sleep.deep_sleep") => { true }; + ("sleep.pd_retention") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c3.rs b/esp-metadata-generated/src/_generated_esp32c3.rs index 42f4a62e458..f3ad0b2f625 100644 --- a/esp-metadata-generated/src/_generated_esp32c3.rs +++ b/esp-metadata-generated/src/_generated_esp32c3.rs @@ -379,6 +379,9 @@ macro_rules! property { ("sleep.deep_sleep") => { true }; + ("sleep.pd_retention") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c5.rs b/esp-metadata-generated/src/_generated_esp32c5.rs index 8d32707ae2c..3d31b8b883d 100644 --- a/esp-metadata-generated/src/_generated_esp32c5.rs +++ b/esp-metadata-generated/src/_generated_esp32c5.rs @@ -391,6 +391,9 @@ macro_rules! property { ("sleep.deep_sleep") => { true }; + ("sleep.pd_retention") => { + false + }; ("soc.cpu_has_branch_predictor") => { true }; diff --git a/esp-metadata-generated/src/_generated_esp32c6.rs b/esp-metadata-generated/src/_generated_esp32c6.rs index f849c379e18..cdb62c3cb4f 100644 --- a/esp-metadata-generated/src/_generated_esp32c6.rs +++ b/esp-metadata-generated/src/_generated_esp32c6.rs @@ -409,6 +409,9 @@ macro_rules! property { ("sleep.deep_sleep") => { true }; + ("sleep.pd_retention") => { + true + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c61.rs b/esp-metadata-generated/src/_generated_esp32c61.rs index 15563166908..458314528cd 100644 --- a/esp-metadata-generated/src/_generated_esp32c61.rs +++ b/esp-metadata-generated/src/_generated_esp32c61.rs @@ -325,6 +325,9 @@ macro_rules! property { ("sleep.deep_sleep") => { true }; + ("sleep.pd_retention") => { + false + }; ("soc.cpu_has_branch_predictor") => { true }; diff --git a/esp-metadata-generated/src/_generated_esp32h2.rs b/esp-metadata-generated/src/_generated_esp32h2.rs index c10039ca75a..a0e6d8e1020 100644 --- a/esp-metadata-generated/src/_generated_esp32h2.rs +++ b/esp-metadata-generated/src/_generated_esp32h2.rs @@ -394,6 +394,9 @@ macro_rules! property { ("sleep.deep_sleep") => { true }; + ("sleep.pd_retention") => { + true + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32p4.rs b/esp-metadata-generated/src/_generated_esp32p4.rs index 1281e8c403e..05fa69bf839 100644 --- a/esp-metadata-generated/src/_generated_esp32p4.rs +++ b/esp-metadata-generated/src/_generated_esp32p4.rs @@ -346,6 +346,9 @@ macro_rules! property { ("sleep.deep_sleep") => { true }; + ("sleep.pd_retention") => { + false + }; ("soc.cpu_has_branch_predictor") => { true }; diff --git a/esp-metadata-generated/src/_generated_esp32s2.rs b/esp-metadata-generated/src/_generated_esp32s2.rs index 34bafc00016..e61e68c0505 100644 --- a/esp-metadata-generated/src/_generated_esp32s2.rs +++ b/esp-metadata-generated/src/_generated_esp32s2.rs @@ -370,6 +370,9 @@ macro_rules! property { ("sleep.deep_sleep") => { true }; + ("sleep.pd_retention") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32s3.rs b/esp-metadata-generated/src/_generated_esp32s3.rs index 3428cba5139..c57561f2e51 100644 --- a/esp-metadata-generated/src/_generated_esp32s3.rs +++ b/esp-metadata-generated/src/_generated_esp32s3.rs @@ -406,6 +406,9 @@ macro_rules! property { ("sleep.deep_sleep") => { true }; + ("sleep.pd_retention") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata/devices/esp32c6/soc.toml b/esp-metadata/devices/esp32c6/soc.toml index f9b80e1f7aa..b84b78a9962 100644 --- a/esp-metadata/devices/esp32c6/soc.toml +++ b/esp-metadata/devices/esp32c6/soc.toml @@ -323,6 +323,9 @@ is_lp_sys = true support_status = "partial" light_sleep = true deep_sleep = true +# esp-hal implements CPU/TOP power-down retention (software CPU retention + +# regDMA TOP retention) for light sleep on this chip. +pd_retention = true wakeup_sources = { Ext1 = 1, Gpio = 2, diff --git a/esp-metadata/devices/esp32h2/soc.toml b/esp-metadata/devices/esp32h2/soc.toml index 6908ef7c31a..00259e2511e 100644 --- a/esp-metadata/devices/esp32h2/soc.toml +++ b/esp-metadata/devices/esp32h2/soc.toml @@ -313,6 +313,9 @@ version = 2 support_status = "partial" light_sleep = true deep_sleep = true +# esp-hal implements CPU/TOP power-down retention (software CPU retention + +# regDMA TOP retention) for light sleep on this chip. +pd_retention = true wakeup_sources = { Ext1 = 1, Gpio = 2, diff --git a/esp-metadata/src/cfg.rs b/esp-metadata/src/cfg.rs index 753e7d28e7e..46f94ca3da9 100644 --- a/esp-metadata/src/cfg.rs +++ b/esp-metadata/src/cfg.rs @@ -733,6 +733,10 @@ driver_configs![ deep_sleep: bool, #[serde(default)] wakeup_sources: WakeupSources, + // esp-hal implements CPU/TOP power-down retention (software CPU retention + + // regDMA TOP retention) for light sleep on this chip. + #[serde(default)] + pd_retention: bool, } }, SocProperties { diff --git a/qa-test/src/bin/sleep_timer_powerdown.rs b/qa-test/src/bin/sleep_timer_powerdown.rs index 72a83247475..df3609c807a 100644 --- a/qa-test/src/bin/sleep_timer_powerdown.rs +++ b/qa-test/src/bin/sleep_timer_powerdown.rs @@ -20,7 +20,7 @@ //! GPIO5 is high while awake, low while asleep, to bracket each sleep on a //! current meter / logic analyzer. -//% CHIP_FILTER: esp32c6 +//% CHIP_FILTER: esp32c6 || esp32h2 #![no_std] #![no_main] From 8f31b9c39c082a679ae9b54b5ae2b2581d4e1603 Mon Sep 17 00:00:00 2001 From: Juraj Sadel Date: Mon, 20 Jul 2026 11:59:43 +0200 Subject: [PATCH 5/8] reviews --- esp-hal/src/rtc_cntl/retention.rs | 49 +++++++++++------------ esp-hal/src/rtc_cntl/retention/esp32c6.rs | 29 ++++++++++++++ esp-hal/src/rtc_cntl/retention/esp32h2.rs | 29 ++++++++++++++ 3 files changed, 81 insertions(+), 26 deletions(-) diff --git a/esp-hal/src/rtc_cntl/retention.rs b/esp-hal/src/rtc_cntl/retention.rs index 210f8d0404c..a36fb45c23e 100644 --- a/esp-hal/src/rtc_cntl/retention.rs +++ b/esp-hal/src/rtc_cntl/retention.rs @@ -47,6 +47,25 @@ pub(crate) use chip::{ PLIC_MX_BASE, PLIC_UX_BASE, }; +// Per-chip opt-in peripheral config-register retention data (register +// offsets/masks/maps/counts). The sequence builders below are chip-agnostic; +// only this register-layout data is device-specific. +use chip::{ + I2C_CONF_UPGATE, + I2C_CTR_OFF, + I2C_FSM_RST, + I2C_REGS_MAP, + I2C_RETENTION_REGS_CNT, + I2C_SCL_LOW_PERIOD_OFF, + SPI_CMD_OFF, + SPI_REGS_MAP, + SPI_RETENTION_REGS_CNT, + UART_INT_ENA_OFF, + UART_REG_UPDATE, + UART_REG_UPDATE_OFF, + UART_REGS_MAP, + UART_RETENTION_REGS_CNT, +}; // Bit layout of `regdma_link_head_t` (see ESP-IDF `regdma.h`): // https://github.com/espressif/esp-idf/blob/v5.4/components/soc/include/soc/regdma.h#L114-L123 @@ -175,15 +194,7 @@ impl RegdmaLink { } // Console UART config-register retention, shared by the always-on console and -// the opt-in `UartRetentionMemory`. ESP-IDF v5.4 `uart_periph.c` -// `UART_SLEEP_RETENTION_ENTRIES`, `uart_reg.h`. -const UART_INT_ENA_OFF: u32 = 0x0C; // UART_INT_ENA_REG -const UART_REG_UPDATE_OFF: u32 = 0x98; // UART_REG_UPDATE_REG -const UART_REG_UPDATE: u32 = 1 << 0; -/// Registers retained (set bits in [`UART_REGS_MAP`]). -const UART_RETENTION_REGS_CNT: u32 = 21; -/// `uart_regs_map[4]`: config registers in the INT_ENA..ID window. -const UART_REGS_MAP: [u32; 4] = [0x007f_ff6d, 0x0000_0010, 0, 0]; +// the opt-in `UartRetentionMemory`. The register data lives in `chip`. /// One ADDR_MAP + a restore-only WRITE+WAIT pulsing `UART_REG_UPDATE`. const UART_NODE_COUNT: usize = 3; @@ -207,17 +218,9 @@ fn build_uart_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { nodes[2] = RegdmaLink::wait(base + UART_REG_UPDATE_OFF, 0, UART_REG_UPDATE, true, false); } -// I2C config-register retention. ESP-IDF v5.4 `i2c_periph.c` -// `i2c0_regs_retention`, `i2c_reg.h`. Config registers are shadowed, so restore +// I2C config-register retention. Config registers are shadowed, so restore // pulses the FSM reset then requests a config update and waits for it to latch. -const I2C_SCL_LOW_PERIOD_OFF: u32 = 0x00; // I2C_SCL_LOW_PERIOD_REG: ADDR_MAP window base -const I2C_CTR_OFF: u32 = 0x04; // I2C_CTR_REG -const I2C_FSM_RST: u32 = 1 << 10; // I2C_FSM_RST (value == mask) -const I2C_CONF_UPGATE: u32 = 1 << 11; // I2C_CONF_UPGATE (value == mask) -/// Registers retained (set bits in [`I2C_REGS_MAP`]). -const I2C_RETENTION_REGS_CNT: u32 = 18; -/// `i2c0_regs_map[4]`: config registers in the `SCL_LOW_PERIOD..SCL_STRETCH_CONF` window. -const I2C_REGS_MAP: [u32; 4] = [0xc03f_345b, 0x3, 0, 0]; +// The register data lives in `chip`. /// One ADDR_MAP + a restore-only WRITE*3/WAIT pulsing `FSM_RST` then `CONF_UPGATE`. const I2C_NODE_COUNT: usize = 5; @@ -238,13 +241,7 @@ fn build_i2c_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { nodes[4] = RegdmaLink::wait(ctr, 0, I2C_CONF_UPGATE, true, false); } -// GPSPI2 config-register retention. ESP-IDF v5.4 `spi_periph.c` -// `spi2_regs_retention`, `spi_reg.h`. -const SPI_CMD_OFF: u32 = 0x00; // SPI_CMD_REG: ADDR_MAP window base -/// Registers retained (set bits in [`SPI_REGS_MAP`]). -const SPI_RETENTION_REGS_CNT: u32 = 12; -/// `spi_regs_map[4]`: config registers in the `CMD..SLAVE` window. -const SPI_REGS_MAP: [u32; 4] = [0x0000_31ff, 0x0100_0000, 0, 0]; +// GPSPI2 config-register retention. The register data lives in `chip`. /// A single ADDR_MAP over the config registers. const SPI_NODE_COUNT: usize = 1; diff --git a/esp-hal/src/rtc_cntl/retention/esp32c6.rs b/esp-hal/src/rtc_cntl/retention/esp32c6.rs index 7643d9b0112..5ca4beaab41 100644 --- a/esp-hal/src/rtc_cntl/retention/esp32c6.rs +++ b/esp-hal/src/rtc_cntl/retention/esp32c6.rs @@ -116,6 +116,35 @@ pub(super) const OPS: &[SysOp] = &[ /// drives the regDMA backup/restore in hardware on the sleep/wake transition. pub(super) const SW_TRIGGER_REGDMA: bool = false; +// Opt-in peripheral config-register retention data (offsets/masks/maps/counts), +// consumed by the chip-agnostic sequence builders in `retention`. +// +// UART: ESP-IDF v5.4 `uart_periph.c` `UART_SLEEP_RETENTION_ENTRIES`, `uart_reg.h`. +pub(super) const UART_INT_ENA_OFF: u32 = 0x0C; // UART_INT_ENA_REG: ADDR_MAP window base +pub(super) const UART_REG_UPDATE_OFF: u32 = 0x98; // UART_REG_UPDATE_REG +pub(super) const UART_REG_UPDATE: u32 = 1 << 0; +/// Registers retained (set bits in [`UART_REGS_MAP`]). +pub(super) const UART_RETENTION_REGS_CNT: u32 = 21; +/// `uart_regs_map[4]`: config registers in the INT_ENA..ID window. +pub(super) const UART_REGS_MAP: [u32; 4] = [0x007f_ff6d, 0x0000_0010, 0, 0]; + +// I2C: ESP-IDF v5.4 `i2c_periph.c` `i2c0_regs_retention`, `i2c_reg.h`. +pub(super) const I2C_SCL_LOW_PERIOD_OFF: u32 = 0x00; // I2C_SCL_LOW_PERIOD_REG: ADDR_MAP window base +pub(super) const I2C_CTR_OFF: u32 = 0x04; // I2C_CTR_REG +pub(super) const I2C_FSM_RST: u32 = 1 << 10; // I2C_FSM_RST (value == mask) +pub(super) const I2C_CONF_UPGATE: u32 = 1 << 11; // I2C_CONF_UPGATE (value == mask) +/// Registers retained (set bits in [`I2C_REGS_MAP`]). +pub(super) const I2C_RETENTION_REGS_CNT: u32 = 18; +/// `i2c0_regs_map[4]`: config registers in the `SCL_LOW_PERIOD..SCL_STRETCH_CONF` window. +pub(super) const I2C_REGS_MAP: [u32; 4] = [0xc03f_345b, 0x3, 0, 0]; + +// GPSPI2: ESP-IDF v5.4 `spi_periph.c` `spi2_regs_retention`, `spi_reg.h`. +pub(super) const SPI_CMD_OFF: u32 = 0x00; // SPI_CMD_REG: ADDR_MAP window base +/// Registers retained (set bits in [`SPI_REGS_MAP`]). +pub(super) const SPI_RETENTION_REGS_CNT: u32 = 12; +/// `spi_regs_map[4]`: config registers in the `CMD..SLAVE` window. +pub(super) const SPI_REGS_MAP: [u32; 4] = [0x0000_31ff, 0x0100_0000, 0, 0]; + // CPU-domain device-register bases lost when `pd_cpu` powers down (consumed by // `cpu_retention`; the region layout around them is shared). pub(crate) const INTPRI_BASE: u32 = 0x600C_5000; // interrupt priority (INTPRI) diff --git a/esp-hal/src/rtc_cntl/retention/esp32h2.rs b/esp-hal/src/rtc_cntl/retention/esp32h2.rs index 25b595f54e0..1bdbf437e75 100644 --- a/esp-hal/src/rtc_cntl/retention/esp32h2.rs +++ b/esp-hal/src/rtc_cntl/retention/esp32h2.rs @@ -121,6 +121,35 @@ pub(super) const OPS: &[SysOp] = &[ // ESP32-H2 use software to trigger REGDMA to restore instead of PMU, because regdma has power bug. pub(super) const SW_TRIGGER_REGDMA: bool = true; +// Opt-in peripheral config-register retention data (offsets/masks/maps/counts), +// consumed by the chip-agnostic sequence builders in `retention`. +// +// UART: ESP-IDF v5.4 `uart_periph.c` `UART_SLEEP_RETENTION_ENTRIES`, `uart_reg.h`. +pub(super) const UART_INT_ENA_OFF: u32 = 0x0C; // UART_INT_ENA_REG: ADDR_MAP window base +pub(super) const UART_REG_UPDATE_OFF: u32 = 0x98; // UART_REG_UPDATE_REG +pub(super) const UART_REG_UPDATE: u32 = 1 << 0; +/// Registers retained (set bits in [`UART_REGS_MAP`]). +pub(super) const UART_RETENTION_REGS_CNT: u32 = 21; +/// `uart_regs_map[4]`: config registers in the INT_ENA..ID window. +pub(super) const UART_REGS_MAP: [u32; 4] = [0x007f_ff6d, 0x0000_0010, 0, 0]; + +// I2C: ESP-IDF v5.4 `i2c_periph.c` `i2c0_regs_retention`, `i2c_reg.h`. +pub(super) const I2C_SCL_LOW_PERIOD_OFF: u32 = 0x00; // I2C_SCL_LOW_PERIOD_REG: ADDR_MAP window base +pub(super) const I2C_CTR_OFF: u32 = 0x04; // I2C_CTR_REG +pub(super) const I2C_FSM_RST: u32 = 1 << 10; // I2C_FSM_RST (value == mask) +pub(super) const I2C_CONF_UPGATE: u32 = 1 << 11; // I2C_CONF_UPGATE (value == mask) +/// Registers retained (set bits in [`I2C_REGS_MAP`]). +pub(super) const I2C_RETENTION_REGS_CNT: u32 = 18; +/// `i2c0_regs_map[4]`: config registers in the `SCL_LOW_PERIOD..SCL_STRETCH_CONF` window. +pub(super) const I2C_REGS_MAP: [u32; 4] = [0xc03f_345b, 0x3, 0, 0]; + +// GPSPI2: ESP-IDF v5.4 `spi_periph.c` `spi2_regs_retention`, `spi_reg.h`. +pub(super) const SPI_CMD_OFF: u32 = 0x00; // SPI_CMD_REG: ADDR_MAP window base +/// Registers retained (set bits in [`SPI_REGS_MAP`]). +pub(super) const SPI_RETENTION_REGS_CNT: u32 = 12; +/// `spi_regs_map[4]`: config registers in the `CMD..SLAVE` window. +pub(super) const SPI_REGS_MAP: [u32; 4] = [0x0000_31ff, 0x0100_0000, 0, 0]; + // CPU-domain device-register bases lost when `pd_cpu` powers down (consumed by // `cpu_retention`). Identical to the C6 (same RISC-V core/cache/PLIC/CLINT). pub(crate) const INTPRI_BASE: u32 = 0x600C_5000; // interrupt priority (INTPRI) From 275a33a97fdb882c010ed6e9d0012d0a42518eb3 Mon Sep 17 00:00:00 2001 From: Juraj Sadel Date: Thu, 30 Jul 2026 18:20:25 +0200 Subject: [PATCH 6/8] PAC rework, more metadata, address review --- esp-hal/Cargo.toml | 4 +- esp-hal/src/rtc_cntl/cpu_retention.rs | 221 ++---- esp-hal/src/rtc_cntl/retention.rs | 655 +++++++++++------- esp-hal/src/rtc_cntl/retention/esp32c6.rs | 223 +++--- esp-hal/src/rtc_cntl/retention/esp32h2.rs | 235 +++---- esp-hal/src/rtc_cntl/sleep/esp32c6.rs | 4 +- esp-hal/src/rtc_cntl/sleep/esp32h2.rs | 7 +- esp-hal/src/soc/csr.rs | 183 +++++ esp-hal/src/soc/mod.rs | 3 + .../src/_build_script_utils.rs | 34 + .../src/_generated_esp32.rs | 6 + .../src/_generated_esp32c2.rs | 6 + .../src/_generated_esp32c3.rs | 6 + .../src/_generated_esp32c5.rs | 6 + .../src/_generated_esp32c6.rs | 6 + .../src/_generated_esp32c61.rs | 6 + .../src/_generated_esp32h2.rs | 6 + .../src/_generated_esp32p4.rs | 6 + .../src/_generated_esp32s2.rs | 6 + .../src/_generated_esp32s3.rs | 6 + esp-metadata/devices/esp32c6/soc.toml | 3 +- esp-metadata/devices/esp32h2/soc.toml | 5 +- esp-metadata/src/cfg.rs | 10 +- esp-metadata/src/cfg/soc.rs | 18 + qa-test/src/bin/sleep_timer_powerdown.rs | 37 +- 25 files changed, 954 insertions(+), 748 deletions(-) create mode 100644 esp-hal/src/soc/csr.rs diff --git a/esp-hal/Cargo.toml b/esp-hal/Cargo.toml index 1eed6879771..ed087d0d055 100644 --- a/esp-hal/Cargo.toml +++ b/esp-hal/Cargo.toml @@ -112,9 +112,9 @@ esp32 = { version = "0.40", features = ["critical-section"], optional = true, esp32c2 = { version = "0.29", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8d0a072" } esp32c3 = { version = "0.32", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8d0a072" } esp32c5 = { version = "0.2", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8d0a072" } -esp32c6 = { version = "0.23", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8d0a072" } +esp32c6 = { version = "0.23", features = ["critical-section"], optional = true, git = "https://github.com/JurajSadel/esp-pacs", branch = "c6-h2-retention-registers" } esp32c61 = { version = "0.3", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8d0a072" } -esp32h2 = { version = "0.19", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8d0a072" } +esp32h2 = { version = "0.19", features = ["critical-section"], optional = true, git = "https://github.com/JurajSadel/esp-pacs", branch = "c6-h2-retention-registers" } esp32s2 = { version = "0.31", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8d0a072" } esp32s3 = { version = "0.35", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8d0a072" } esp32s31 = { version = "0.1", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8d0a072" } diff --git a/esp-hal/src/rtc_cntl/cpu_retention.rs b/esp-hal/src/rtc_cntl/cpu_retention.rs index af4f5205113..be115439d33 100644 --- a/esp-hal/src/rtc_cntl/cpu_retention.rs +++ b/esp-hal/src/rtc_cntl/cpu_retention.rs @@ -1,17 +1,4 @@ //! CPU power-down retention during light sleep (RISC-V PMU chips). -//! -//! When `pd_cpu` powers down, the CPU loses all state and regDMA can't reach the -//! register file/CSRs, so they are saved/restored in software in three parts: -//! critical registers (GP + machine CSRs) in assembly via a setjmp/longjmp-style -//! trick, non-critical CSRs via `csrr`/`csrw`, and CPU-domain device registers -//! (`INTPRI`, `PLIC`/`CLINT`, cache). Backing RAM ([`CpuRetentionMemory`]) is -//! caller-owned, opt-in via -//! [`RtcSleepConfig::with_cpu_power_down`](crate::rtc_cntl::sleep::RtcSleepConfig::with_cpu_power_down). -//! -//! The logic is chip-agnostic - only the device-register base addresses are -//! per-chip data (from the `retention::chip` module). Everything runs from IRAM -//! (`.rwtext`): the ROM wakes with the flash cache lost, so no `#[ram]` function -//! may call flash-resident code until the cache config is restored. // Mirrors ESP-IDF `v5.4` `esp_sleep_cpu_retention()` (`sleep_cpu.c`, // `sleep_cpu_asm.S`, `rvsleep-frames.h`). @@ -20,17 +7,19 @@ use core::sync::atomic::{AtomicU32, Ordering}; use procmacros::ram; -use crate::peripherals::{LP_AON, PMU}; /// Second buffer required by /// [`RtcSleepConfig::with_top_power_down`](crate::rtc_cntl::sleep::RtcSleepConfig::with_top_power_down). #[instability::unstable] pub use crate::rtc_cntl::retention::SystemRetentionMemory; +use crate::{ + peripherals::{LP_AON, PMU}, + soc::csr, +}; /// Incremented only when the CPU domain actually lost and regained power. static CPU_POWERDOWN_WAKES: AtomicU32 = AtomicU32::new(0); /// How many times the CPU power domain was actually powered down and restored. -/// Diagnostic: rises across light sleeps only if the CPU genuinely lost power. #[instability::unstable] pub fn cpu_power_down_wake_count() -> u32 { CPU_POWERDOWN_WAKES.load(Ordering::Relaxed) @@ -253,38 +242,19 @@ rv_core_critical_regs_restore: // Non-critical CSRs (RvCoreNonCriticalSleepFrame) // --------------------------------------------------------------------------- -/// Read a CSR by numeric address (must be a compile-time constant). -#[inline(always)] -unsafe fn read_csr() -> u32 { - let value: u32; - unsafe { - core::arch::asm!("csrr {0}, {1}", out(reg) value, const CSR, options(nostack)); - } - value -} - -/// Write a CSR by numeric address (must be a compile-time constant). -#[inline(always)] -unsafe fn write_csr(value: u32) { - unsafe { - core::arch::asm!("csrw {1}, {0}", in(reg) value, const CSR, options(nostack)); - } -} - /// Generate the slot count and save/restore routines for the non-critical CSRs -/// from one list. `$name` is documentation only; CSRs are addressed by number -/// so custom Espressif CSRs need no assembler support. +/// from one list of names. // CSR list order matches ESP-IDF `sleep_cpu.c`. macro_rules! noncritical_csrs { - ($($name:ident = $csr:literal),+ $(,)?) => { + ($($name:ident),+ $(,)?) => { /// Non-critical CSR slot count; sizes the `noncritical` field. - const NONCRITICAL_WORDS: usize = [$($csr),+].len(); + const NONCRITICAL_WORDS: usize = [$(stringify!($name)),+].len(); #[ram] fn save_noncritical(buf: *mut u32) { let mut i = 0usize; $( - unsafe { buf.add(i).write(read_csr::<$csr>()); } + unsafe { buf.add(i).write(csr::$name::read() as u32); } i += 1; )+ let _ = i; @@ -294,7 +264,7 @@ macro_rules! noncritical_csrs { fn restore_noncritical(buf: *const u32) { let mut i = 0usize; $( - unsafe { write_csr::<$csr>(buf.add(i).read()); } + unsafe { csr::$name::write(buf.add(i).read() as usize); } i += 1; )+ let _ = i; @@ -303,144 +273,60 @@ macro_rules! noncritical_csrs { } noncritical_csrs! { - mscratch = 0x340, - mideleg = 0x303, - misa = 0x301, - tselect = 0x7A0, - tdata1 = 0x7A1, - tdata2 = 0x7A2, - tcontrol = 0x7A5, - pmpaddr0 = 0x3B0, pmpaddr1 = 0x3B1, pmpaddr2 = 0x3B2, pmpaddr3 = 0x3B3, - pmpaddr4 = 0x3B4, pmpaddr5 = 0x3B5, pmpaddr6 = 0x3B6, pmpaddr7 = 0x3B7, - pmpaddr8 = 0x3B8, pmpaddr9 = 0x3B9, pmpaddr10 = 0x3BA, pmpaddr11 = 0x3BB, - pmpaddr12 = 0x3BC, pmpaddr13 = 0x3BD, pmpaddr14 = 0x3BE, pmpaddr15 = 0x3BF, - pmpcfg0 = 0x3A0, pmpcfg1 = 0x3A1, pmpcfg2 = 0x3A2, pmpcfg3 = 0x3A3, - pmaaddr0 = 0xBD0, pmaaddr1 = 0xBD1, pmaaddr2 = 0xBD2, pmaaddr3 = 0xBD3, - pmaaddr4 = 0xBD4, pmaaddr5 = 0xBD5, pmaaddr6 = 0xBD6, pmaaddr7 = 0xBD7, - pmaaddr8 = 0xBD8, pmaaddr9 = 0xBD9, pmaaddr10 = 0xBDA, pmaaddr11 = 0xBDB, - pmaaddr12 = 0xBDC, pmaaddr13 = 0xBDD, pmaaddr14 = 0xBDE, pmaaddr15 = 0xBDF, - pmacfg0 = 0xBC0, pmacfg1 = 0xBC1, pmacfg2 = 0xBC2, pmacfg3 = 0xBC3, - pmacfg4 = 0xBC4, pmacfg5 = 0xBC5, pmacfg6 = 0xBC6, pmacfg7 = 0xBC7, - pmacfg8 = 0xBC8, pmacfg9 = 0xBC9, pmacfg10 = 0xBCA, pmacfg11 = 0xBCB, - pmacfg12 = 0xBCC, pmacfg13 = 0xBCD, pmacfg14 = 0xBCE, pmacfg15 = 0xBCF, - utvec = 0x005, - ustatus = 0x000, - uepc = 0x041, - ucause = 0x042, - mpcer = 0x7E0, - mpcmr = 0x7E1, - mpccr = 0x7E2, - cpu_testbus_ctrl = 0x7E3, - upcer = 0x800, - upcmr = 0x801, - upccr = 0x802, - ugpio_oen = 0x803, - ugpio_in = 0x804, - ugpio_out = 0x805, + mscratch, + mideleg, + misa, + tselect, + tdata1, + tdata2, + tcontrol, + pmpaddr0, pmpaddr1, pmpaddr2, pmpaddr3, + pmpaddr4, pmpaddr5, pmpaddr6, pmpaddr7, + pmpaddr8, pmpaddr9, pmpaddr10, pmpaddr11, + pmpaddr12, pmpaddr13, pmpaddr14, pmpaddr15, + pmpcfg0, pmpcfg1, pmpcfg2, pmpcfg3, + pmaaddr0, pmaaddr1, pmaaddr2, pmaaddr3, + pmaaddr4, pmaaddr5, pmaaddr6, pmaaddr7, + pmaaddr8, pmaaddr9, pmaaddr10, pmaaddr11, + pmaaddr12, pmaaddr13, pmaaddr14, pmaaddr15, + pmacfg0, pmacfg1, pmacfg2, pmacfg3, + pmacfg4, pmacfg5, pmacfg6, pmacfg7, + pmacfg8, pmacfg9, pmacfg10, pmacfg11, + pmacfg12, pmacfg13, pmacfg14, pmacfg15, + utvec, + ustatus, + uepc, + ucause, + mpcer, + mpcmr, + mpccr, + cpu_testbus_ctrl, + upcer, + upcmr, + upccr, + ugpio_oen, + ugpio_in, + ugpio_out, } // --------------------------------------------------------------------------- // CPU-domain device registers (INTPRI / cache / PLIC / CLINT) // --------------------------------------------------------------------------- -/// A contiguous run of `words` 32-bit registers starting at `start`. -struct Region { - start: u32, - words: usize, -} - -/// Total 32-bit words covered by a set of [`Region`]s, to size their store. -const fn total_words(regions: &[Region]) -> usize { - let mut words = 0; - let mut i = 0; - while i < regions.len() { - words += regions[i].words; - i += 1; - } - words -} - -// Per-chip base addresses (the region layout below is chip-agnostic). use crate::rtc_cntl::retention::{ - CACHE_BASE, - CLINT_MINT_BASE, - CLINT_UINT_BASE, - INTPRI_BASE, - PLIC_MX_BASE, - PLIC_UX_BASE, + CACHE_REGIONS, + CLINT_REGIONS, + INTPRI_REGIONS, + PLIC_REGIONS, + Region, + total_words, }; -// Interrupt matrix priority registers (`INTPRI`). -const INTPRI_REGIONS: [Region; 2] = [ - // INTPRI_CORE0_CPU_INT_ENABLE_REG ..= INTPRI_RND_ECO_LOW_REG - Region { - start: INTPRI_BASE, - words: 45, - }, - // INTPRI_RND_ECO_HIGH_REG - Region { - start: INTPRI_BASE + 0x3FC, - words: 1, - }, -]; - -// L1 cache control (`EXTMEM`/`CACHE`). -const CACHE_REGIONS: [Region; 2] = [ - // *_L1_CACHE_CTRL_REG - Region { - start: CACHE_BASE + 0x4, - words: 1, - }, - // *_L1_CACHE_WRAP_AROUND_CTRL_REG - Region { - start: CACHE_BASE + 0x20, - words: 1, - }, -]; - -// PLIC machine/user interrupt controllers. -const PLIC_REGIONS: [Region; 4] = [ - // PLIC_MXINT_ENABLE_REG ..= PLIC_MXINT_CLAIM_REG - Region { - start: PLIC_MX_BASE, - words: 38, - }, - // PLIC_MXINT_CONF_REG - Region { - start: PLIC_MX_BASE + 0x3FC, - words: 1, - }, - // PLIC_UXINT_ENABLE_REG ..= PLIC_UXINT_CLAIM_REG - Region { - start: PLIC_UX_BASE, - words: 38, - }, - // PLIC_UXINT_CONF_REG - Region { - start: PLIC_UX_BASE + 0x3FC, - words: 1, - }, -]; - -// CLINT machine/user timers. -const CLINT_REGIONS: [Region; 2] = [ - // CLINT_MINT_SIP_REG ..= CLINT_MINT_MTIMECMP_H_REG - Region { - start: CLINT_MINT_BASE, - words: 6, - }, - // CLINT_UINT_SIP_REG ..= CLINT_UINT_UTIMECMP_H_REG - Region { - start: CLINT_UINT_BASE, - words: 6, - }, -]; - #[ram] fn save_device_regs(regions: &[Region], buf: *mut u32) { let mut out = buf; for region in regions { - let mut addr = region.start as *const u32; + let mut addr = (region.start)() as *const u32; for _ in 0..region.words { unsafe { out.write(addr.read_volatile()); @@ -455,7 +341,7 @@ fn save_device_regs(regions: &[Region], buf: *mut u32) { fn restore_device_regs(regions: &[Region], buf: *const u32) { let mut src = buf; for region in regions { - let mut addr = region.start as *mut u32; + let mut addr = (region.start)() as *mut u32; for _ in 0..region.words { unsafe { addr.write_volatile(src.read()); @@ -470,7 +356,7 @@ fn restore_device_regs(regions: &[Region], buf: *const u32) { // Caller-owned retention storage // --------------------------------------------------------------------------- -/// Backing storage (~1 KiB) for CPU power-down register retention. +/// Backing storage for CPU power-down register retention. /// /// Caller-owned, opted into via [`RtcSleepConfig::with_cpu_power_down`] (or /// [`RtcSleepConfig::with_top_power_down`], which also powers the CPU down). @@ -520,7 +406,8 @@ impl Default for CpuRetentionMemory { // --------------------------------------------------------------------------- /// Read `mstatus` and clear its global machine-interrupt-enable bit (`MIE`), -/// returning the previous value. Mirrors `RV_READ_MSTATUS_AND_DISABLE_INTR()`. +/// returning the previous value. +// Mirrors `RV_READ_MSTATUS_AND_DISABLE_INTR()`. #[inline(always)] unsafe fn save_mstatus_and_disable_int() -> u32 { let mstatus: u32; diff --git a/esp-hal/src/rtc_cntl/retention.rs b/esp-hal/src/rtc_cntl/retention.rs index a36fb45c23e..5f61880868d 100644 --- a/esp-hal/src/rtc_cntl/retention.rs +++ b/esp-hal/src/rtc_cntl/retention.rs @@ -5,10 +5,6 @@ //! list of [`RegdmaLink`] nodes on PAU entry link 0. Arming builds the core set //! into a caller-owned [`SystemRetentionMemory`], chains any opt-in peripheral //! entries and programs the link. -//! -//! All logic here is chip-agnostic; the only per-chip input is register data -//! (the `OPS` program and the base addresses), which lives in the `chip` -//! submodule - one data file per chip. Adding a chip is a new data file. // References (ESP-IDF `v5.4`): `soc/regdma.h`, `hal//pau_ll.h`, // `hal//pau_hal.c`, `esp_hw_support/port/pau_regdma.c`. @@ -30,42 +26,231 @@ use crate::{ }, }; -// Per-chip register data (base addresses, region sizes, the SYS_PERIPH program -// and the CPU-domain device-register bases). Selected by target; consumed only -// by the chip-agnostic logic here and (via the re-export below) `cpu_retention`. +/// Runtime absolute address (as `u32`) of a named PAC register. +macro_rules! reg_addr { + ($peri:ident, $($path:tt)+) => { + (unsafe { &*crate::pac::$peri::PTR }.$($path)+.as_ptr()) as u32 + }; +} + +/// Runtime base address (as `u32`) of a PAC peripheral. +macro_rules! peri_base { + ($peri:ident) => { + crate::pac::$peri::ptr() as u32 + }; +} + +/// `const` byte offset of a named PAC register within its peripheral. +macro_rules! reg_off { + ($peri:ident, $($path:tt)+) => {{ + let block = core::mem::MaybeUninit::< + ::Target, + >::uninit(); + let base = block.as_ptr(); + let reg = unsafe { (*base).$($path)+ }; + + (unsafe { (reg as *const _ as *const u8).offset_from(base as *const u8) }) as u32 + }}; +} + +macro_rules! reg { + ($peri:ident, $($path:tt)+) => { + || reg_addr!($peri, $($path)+) + }; +} + +/// `const` width of a named PAC register in 32-bit words: `2` for the 64-bit +/// CLINT counters, `1` for everything else. +macro_rules! reg_words { + ($peri:ident, $($path:tt)+) => {{ + let block = core::mem::MaybeUninit::< + ::Target, + >::uninit(); + let base = block.as_ptr(); + let reg = unsafe { (*base).$($path)+ }; + (core::mem::size_of_val(reg) / 4) as u32 + }}; +} + +/// `const` count of registers spanning `[from] ..= [to]` (inclusive). +macro_rules! span { + ($peri:ident, [$($from:tt)+] ..= [$($to:tt)+]) => { + (reg_off!($peri, $($to)+) - reg_off!($peri, $($from)+)) / 4 + + reg_words!($peri, $($to)+) + }; +} + +// --------------------------------------------------------------------------- +// `SysOp` builders. Each retention step is one self-describing line whose +// address(es) and register count are both derived from the named register(s). +// --------------------------------------------------------------------------- + +/// A `Continuous` op. Two forms: +/// - `continuous!(PCR, [uart(0).conf()] ..= [sram_power_conf()])` - back up the inclusive register +/// span; `count` is derived from it. +/// - `continuous!(GPIO, [func_out_sel_cfg(0)], 35)` - named start, explicit count (used where the +/// end register isn't modelled in the PAC). +macro_rules! continuous { + ($peri:ident, [$($from:tt)+] ..= [$($to:tt)+]) => { + SysOp::Continuous { + addr: || reg_addr!($peri, $($from)+), + count: span!($peri, [$($from)+] ..= [$($to)+]), + } + }; + ($peri:ident, [$($from:tt)+], $count:expr) => { + SysOp::Continuous { + addr: || reg_addr!($peri, $($from)+), + count: $count, + } + }; +} + +/// A `ContinuousSplit` op: back up `[backup]`, restore into `[restore]`. +macro_rules! continuous_split { + ($peri:ident, [$($backup:tt)+] => [$($restore:tt)+], $count:expr) => { + SysOp::ContinuousSplit { + backup: || reg_addr!($peri, $($backup)+), + restore: || reg_addr!($peri, $($restore)+), + count: $count, + } + }; +} + +/// A restore-only masked `Write`. +macro_rules! write_reg { + ($peri:ident, [$($path:tt)+], $value:expr, $mask:expr) => { + SysOp::Write { + addr: || reg_addr!($peri, $($path)+), + value: $value, + mask: $mask, + } + }; +} + +/// A restore-only `Wait` until `(reg & mask) == value`. +#[cfg(sleep_regdma_wait_ops)] +macro_rules! wait_reg { + ($peri:ident, [$($path:tt)+], $value:expr, $mask:expr) => { + SysOp::Wait { + addr: || reg_addr!($peri, $($path)+), + value: $value, + mask: $mask, + } + }; +} + +/// The shared console-UART sequence for the given UART peripheral. +macro_rules! uart_seq { + ($peri:ident) => { + SysOp::Uart { + base: || peri_base!($peri), + } + }; +} + +/// The shared SysTimer sequence for the given SysTimer peripheral. +macro_rules! systimer_seq { + ($peri:ident) => { + SysOp::Systimer { + base: || peri_base!($peri), + } + }; +} + +// --------------------------------------------------------------------------- +// `PeriphOp` builders. Opt-in peripheral (UART/I2C/SPI) retention chains are +// per-chip, per-peripheral `&[PeriphOp]` lists. +// --------------------------------------------------------------------------- + +/// Back up/restore a run of config registers. Two forms: +/// - `periph_continuous!(UART0, [hwfc_conf()] ..= [tout_conf()])` +/// - `periph_continuous!(UART0, [int_ena()])` +macro_rules! periph_continuous { + ($peri:ident, [$($from:tt)+] ..= [$($to:tt)+]) => { + PeriphOp::Continuous { + off: reg_off!($peri, $($from)+), + count: span!($peri, [$($from)+] ..= [$($to)+]), + } + }; + ($peri:ident, [$($reg:tt)+]) => { + PeriphOp::Continuous { + off: reg_off!($peri, $($reg)+), + count: 1, + } + }; +} + +/// A restore-only masked write to a named config register. +macro_rules! periph_write { + ($peri:ident, [$($reg:tt)+], $value:expr, $mask:expr) => { + PeriphOp::Write { + off: reg_off!($peri, $($reg)+), + value: $value, + mask: $mask, + } + }; +} + +/// A restore-only poll of a named config register until `(reg & mask) == value`. +macro_rules! periph_wait { + ($peri:ident, [$($reg:tt)+], $value:expr, $mask:expr) => { + PeriphOp::Wait { + off: reg_off!($peri, $($reg)+), + value: $value, + mask: $mask, + } + }; +} + +// --------------------------------------------------------------------------- +// CPU-domain register regions, copied word by word by `cpu_retention` because +// regDMA cannot reach them while the CPU domain is off. +// --------------------------------------------------------------------------- + +/// A contiguous run of `words` 32-bit registers starting at `start`. +/// +/// `start` is resolved from the PAC at runtime (see [`reg!`]); `words` stays +/// `const` so the retention store can be sized at compile time. +pub(crate) struct Region { + pub(crate) start: fn() -> u32, + pub(crate) words: usize, +} + +/// Total 32-bit words covered by a set of [`Region`]s, to size their store. +pub(crate) const fn total_words(regions: &[Region]) -> usize { + let mut words = 0; + let mut i = 0; + while i < regions.len() { + words += regions[i].words; + i += 1; + } + words +} + +/// A [`Region`] over named registers. Two forms: +/// - `region!(INTPRI, [cpu_int_enable()] ..= [rnd_eco_low()])` +/// - `region!(PLIC_MX, [mxint_conf()])` +macro_rules! region { + ($peri:ident, [$($from:tt)+] ..= [$($to:tt)+]) => { + Region { + start: reg!($peri, $($from)+), + words: span!($peri, [$($from)+] ..= [$($to)+]) as usize, + } + }; + ($peri:ident, [$($reg:tt)+]) => { + Region { + start: reg!($peri, $($reg)+), + words: reg_words!($peri, $($reg)+) as usize, + } + }; +} + #[cfg_attr(esp32c6, path = "retention/esp32c6.rs")] #[cfg_attr(esp32h2, path = "retention/esp32h2.rs")] mod chip; -// The CPU-domain device-register bases live in the same per-chip data module; -// re-export them so `cpu_retention` can read them while `chip` stays private. -pub(crate) use chip::{ - CACHE_BASE, - CLINT_MINT_BASE, - CLINT_UINT_BASE, - INTPRI_BASE, - PLIC_MX_BASE, - PLIC_UX_BASE, -}; -// Per-chip opt-in peripheral config-register retention data (register -// offsets/masks/maps/counts). The sequence builders below are chip-agnostic; -// only this register-layout data is device-specific. -use chip::{ - I2C_CONF_UPGATE, - I2C_CTR_OFF, - I2C_FSM_RST, - I2C_REGS_MAP, - I2C_RETENTION_REGS_CNT, - I2C_SCL_LOW_PERIOD_OFF, - SPI_CMD_OFF, - SPI_REGS_MAP, - SPI_RETENTION_REGS_CNT, - UART_INT_ENA_OFF, - UART_REG_UPDATE, - UART_REG_UPDATE_OFF, - UART_REGS_MAP, - UART_RETENTION_REGS_CNT, -}; +pub(crate) use chip::{CACHE_REGIONS, CLINT_REGIONS, INTPRI_REGIONS, PLIC_REGIONS}; +use chip::{I2C_OPS, SPI_OPS, UART_OPS}; // Bit layout of `regdma_link_head_t` (see ESP-IDF `regdma.h`): // https://github.com/espressif/esp-idf/blob/v5.4/components/soc/include/soc/regdma.h#L114-L123 @@ -81,20 +266,15 @@ const HEAD_EOF_BIT: u32 = 1 << 31; // end of link enum LinkMode { /// Back up/restore a run of consecutive registers via a RAM buffer. Continuous = 0, - /// Like [`Continuous`](Self::Continuous), but a 4-word bitmap selects which - /// registers in the window to transfer (skipping interspersed read-only - /// status/FIFO registers). - AddrMap = 1, /// Unconditionally write a masked value to a register. Write = 2, /// Poll a register until `(reg & mask) == value`. Wait = 3, } -/// A single regDMA linked-list node (matches the PAU `head` + body layout). +/// A single regDMA linked-list node. /// /// - CONTINUOUS: `w0 = backup addr`, `w1 = restore addr`, `w2 = RAM buffer`. -/// - ADDR_MAP: as CONTINUOUS, plus `map` selecting which registers to transfer. /// - WRITE/WAIT: `w0 = target addr`, `w1 = value`, `w2 = mask`. #[repr(C, align(4))] #[derive(Clone, Copy, Debug)] @@ -107,8 +287,6 @@ pub(crate) struct RegdmaLink { w0: u32, w1: u32, w2: u32, - /// ADDR_MAP register-selection bitmap; unread for other modes. - map: [u32; 4], } impl RegdmaLink { @@ -118,7 +296,6 @@ impl RegdmaLink { w0: 0, w1: 0, w2: 0, - map: [0; 4], }; fn head(mode: LinkMode, len: u32, skip_b: bool, skip_r: bool) -> u32 { @@ -138,7 +315,7 @@ impl RegdmaLink { } /// A CONTINUOUS node with distinct backup/restore registers sharing one RAM - /// buffer (e.g. the SysTimer value vs. load registers). + /// buffer. fn continuous_split(backup: u32, restore: u32, storage: u32, len: u32) -> Self { Self { head: Self::head(LinkMode::Continuous, len, false, false), @@ -146,20 +323,6 @@ impl RegdmaLink { w0: backup, w1: restore, w2: storage, - map: [0; 4], - } - } - - /// An ADDR_MAP node: back up/restore the `count` registers selected by `map` - /// (bit `i` = register at `reg + i * 4`) from `reg` into `storage`. - fn addr_map(reg: u32, storage: u32, count: u32, map: [u32; 4]) -> Self { - Self { - head: Self::head(LinkMode::AddrMap, count, false, false), - next: 0, - w0: reg, - w1: reg, - w2: storage, - map, } } @@ -172,7 +335,6 @@ impl RegdmaLink { w0: target, w1: value, w2: mask, - map: [0; 4], } } @@ -184,7 +346,6 @@ impl RegdmaLink { w0: target, w1: value, w2: mask, - map: [0; 4], } } @@ -193,80 +354,67 @@ impl RegdmaLink { } } -// Console UART config-register retention, shared by the always-on console and -// the opt-in `UartRetentionMemory`. The register data lives in `chip`. -/// One ADDR_MAP + a restore-only WRITE+WAIT pulsing `UART_REG_UPDATE`. -const UART_NODE_COUNT: usize = 3; - -/// Build the UART retention sequence for `base` into `nodes`, backing the -/// registers up into `storage`. The WRITE+WAIT pulse the update bit on restore -/// to latch the shadow (`_SYNC`) registers. -fn build_uart_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { - nodes[0] = RegdmaLink::addr_map( - base + UART_INT_ENA_OFF, - storage, - UART_RETENTION_REGS_CNT, - UART_REGS_MAP, - ); - nodes[1] = RegdmaLink::write( - base + UART_REG_UPDATE_OFF, - UART_REG_UPDATE, - UART_REG_UPDATE, - true, - false, - ); - nodes[2] = RegdmaLink::wait(base + UART_REG_UPDATE_OFF, 0, UART_REG_UPDATE, true, false); +/// One step of an opt-in peripheral's config-register retention chain. +/// +/// Unlike [`SysOp`] (whose peripherals are fixed, so it carries absolute +/// addresses) these apply to whichever instance the caller registers. +#[derive(Clone, Copy)] +enum PeriphOp { + /// Back up/restore `count` consecutive registers at `base + off`. + Continuous { off: u32, count: u32 }, + /// Restore-only masked write of `value` to `base + off`. + Write { off: u32, value: u32, mask: u32 }, + /// Restore-only poll of `base + off` until `(reg & mask) == value`. + Wait { off: u32, value: u32, mask: u32 }, } -// I2C config-register retention. Config registers are shadowed, so restore -// pulses the FSM reset then requests a config update and waits for it to latch. -// The register data lives in `chip`. -/// One ADDR_MAP + a restore-only WRITE*3/WAIT pulsing `FSM_RST` then `CONF_UPGATE`. -const I2C_NODE_COUNT: usize = 5; - -/// Build the I2C retention sequence for `base` into `nodes`, backing the -/// registers up into `storage`. -fn build_i2c_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { - let ctr = base + I2C_CTR_OFF; - nodes[0] = RegdmaLink::addr_map( - base + I2C_SCL_LOW_PERIOD_OFF, - storage, - I2C_RETENTION_REGS_CNT, - I2C_REGS_MAP, - ); - // Restore-only: pulse FSM reset, request config update, wait for it to latch. - nodes[1] = RegdmaLink::write(ctr, I2C_FSM_RST, I2C_FSM_RST, true, false); - nodes[2] = RegdmaLink::write(ctr, 0, I2C_FSM_RST, true, false); - nodes[3] = RegdmaLink::write(ctr, I2C_CONF_UPGATE, I2C_CONF_UPGATE, true, false); - nodes[4] = RegdmaLink::wait(ctr, 0, I2C_CONF_UPGATE, true, false); +/// PAU nodes an opt-in peripheral op list expands to (one per op). +const fn periph_nodes(ops: &[PeriphOp]) -> usize { + ops.len() } -// GPSPI2 config-register retention. The register data lives in `chip`. -/// A single ADDR_MAP over the config registers. -const SPI_NODE_COUNT: usize = 1; +/// RAM buffer words an opt-in peripheral op list needs (its backed-up registers). +const fn periph_words(ops: &[PeriphOp]) -> usize { + let mut w = 0; + let mut i = 0; + while i < ops.len() { + if let PeriphOp::Continuous { count, .. } = ops[i] { + w += count as usize; + } + i += 1; + } + w +} -/// Build the SPI retention sequence for `base` into `nodes`, backing the -/// registers up into `storage`. -/// -/// The config registers are only reachable while the SPI function clock runs. -/// `Spi::with_retention_memory` holds that clock for the retention lifetime, so -/// they stay accessible at both backup and restore. -// `spi2_regs_retention`) -fn build_spi_seq(base: u32, nodes: &mut [RegdmaLink], storage: u32) { - nodes[0] = RegdmaLink::addr_map( - base + SPI_CMD_OFF, - storage, - SPI_RETENTION_REGS_CNT, - SPI_REGS_MAP, - ); +/// Build a peripheral's retention sequence for `base` into `nodes`, drawing its +/// CONTINUOUS RAM from `storage`. Fills exactly [`periph_nodes`] nodes and +/// consumes [`periph_words`] words. +fn build_periph_seq(base: u32, ops: &[PeriphOp], nodes: &mut [RegdmaLink], storage: *mut u32) { + let mut word = 0; + for (node, op) in nodes.iter_mut().zip(ops) { + *node = match *op { + PeriphOp::Continuous { off, count } => { + let mem = unsafe { storage.add(word) } as u32; + word += count as usize; + RegdmaLink::continuous(base + off, mem, count) + } + PeriphOp::Write { off, value, mask } => { + RegdmaLink::write(base + off, value, mask, true, false) + } + PeriphOp::Wait { off, value, mask } => { + RegdmaLink::wait(base + off, value, mask, true, false) + } + }; + } } +/// Console UART config-register retention, shared by the always-on console and +/// the opt-in `UartRetentionMemory`. +const UART_NODE_COUNT: usize = periph_nodes(chip::UART_OPS); +/// RAM buffer words the UART chain backs up (see [`chip::UART_OPS`]). +const UART_WORDS: usize = periph_words(chip::UART_OPS); + /// A node in the intrusive registry of opt-in peripheral retention sequences. -/// -/// One lives inside each peripheral's caller-owned retention memory, so any -/// number of peripherals can register without a fixed table or allocation. The -/// pointers are only dereferenced in [`arm_link`] (under [`REGISTRY`], on the -/// single HP core), pointing at borrow-frozen memory until deregistered. pub(crate) struct RetentionNode { next: Option>, head: *mut RegdmaLink, @@ -299,8 +447,6 @@ impl defmt::Format for RetentionNode { /// Head of the intrusive list of registered peripheral retention sequences. struct Registry(Option>); -// SAFETY: the pointers are only followed under the `REGISTRY` lock, and while -// armed they point at borrow-frozen caller memory on the single HP core. unsafe impl Send for Registry {} static REGISTRY: NonReentrantMutex = NonReentrantMutex::new(Registry(None)); @@ -322,8 +468,7 @@ fn deregister_node(node: &mut RetentionNode) { let target = NonNull::from(&mut *node); REGISTRY.with(|registry| { let mut link: *mut Option> = &mut registry.0; - // SAFETY: every pointer in the list points at a live, registered node; - // the walk only follows `next` links until it reaches `target`. + unsafe { while let Some(current) = *link { if current == target { @@ -358,8 +503,6 @@ fn arm_link(core: &mut [RegdmaLink]) -> u32 { REGISTRY.with(|registry| { let mut current = registry.0; while let Some(node) = current { - // SAFETY: a registered node points at a live, borrow-frozen - // caller-owned array of `len` nodes, distinct from every other. let (seg_head, seg_len, next) = unsafe { let node = node.as_ref(); (node.head, node.len, node.next) @@ -381,10 +524,10 @@ fn arm_link(core: &mut [RegdmaLink]) -> u32 { head } -/// Generate a per-peripheral caller-owned regDMA backing store, sized to its -/// `nodes` and register `buf`. `$build` is its sequence builder. +/// Generate a per-peripheral caller-owned regDMA backing store, sized from and +/// built by the chip's `$ops` op chain. macro_rules! peripheral_retention_memory { - ($name:ident, $nodes:expr, $words:expr, $build:path, $doc:expr) => { + ($name:ident, $ops:expr, $doc:expr) => { #[doc = $doc] #[instability::unstable] #[derive(Debug)] @@ -392,8 +535,8 @@ macro_rules! peripheral_retention_memory { #[repr(C, align(4))] pub struct $name { node: RetentionNode, - nodes: [RegdmaLink; $nodes], - buf: [u32; $words], + nodes: [RegdmaLink; periph_nodes($ops)], + buf: [u32; periph_words($ops)], } #[instability::unstable] @@ -409,16 +552,16 @@ macro_rules! peripheral_retention_memory { pub const fn new() -> Self { Self { node: RetentionNode::new(), - nodes: [RegdmaLink::EMPTY; $nodes], - buf: [0; $words], + nodes: [RegdmaLink::EMPTY; periph_nodes($ops)], + buf: [0; periph_words($ops)], } } } impl RetentionMemory for $name { fn register(&mut self, base: u32) -> NonNull { - let storage = self.buf.as_mut_ptr() as u32; - $build(base, &mut self.nodes, storage); + let storage = self.buf.as_mut_ptr(); + build_periph_seq(base, $ops, &mut self.nodes, storage); register_node(&mut self.node, &mut self.nodes) } } @@ -427,9 +570,7 @@ macro_rules! peripheral_retention_memory { peripheral_retention_memory!( UartRetentionMemory, - UART_NODE_COUNT, - UART_RETENTION_REGS_CNT as usize, - build_uart_seq, + UART_OPS, "Caller-owned store retaining one UART's config registers across a `TOP` \ power-down, passed to \ [`Uart::with_retention_memory`](crate::uart::Uart::with_retention_memory). The \ @@ -438,9 +579,7 @@ console/log UART is retained automatically." peripheral_retention_memory!( I2cRetentionMemory, - I2C_NODE_COUNT, - I2C_RETENTION_REGS_CNT as usize, - build_i2c_seq, + I2C_OPS, "Caller-owned store retaining one I2C's config registers across a `TOP` \ power-down, passed to \ [`I2c::with_retention_memory`](crate::i2c::master::I2c::with_retention_memory). \ @@ -449,9 +588,7 @@ See [`UartRetentionMemory`]." peripheral_retention_memory!( SpiRetentionMemory, - SPI_NODE_COUNT, - SPI_RETENTION_REGS_CNT as usize, - build_spi_seq, + SPI_OPS, "Caller-owned store retaining one SPI's config registers across a `TOP` \ power-down, passed to \ [`Spi::with_retention_memory`](crate::spi::master::Spi::with_retention_memory). \ @@ -459,21 +596,18 @@ See [`UartRetentionMemory`]." ); /// Caller-owned retention memory that can be registered for TOP-domain -/// retention. Implemented by the generated `*RetentionMemory` types. +/// retention. pub(crate) trait RetentionMemory { /// Build the retention sequence for `base` and register it, returning its /// registry node. fn register(&mut self, base: u32) -> NonNull; } -/// A `TOP`-domain driver's power state, stored in the driver: either active, -/// holding a [`PowerDomainLock`] that keeps `TOP` powered (without preventing -/// sleep), or retained, with the lock dropped and regDMA saving/restoring its -/// config across a `TOP` power-down from `'d`-borrowed memory. +/// A `TOP`-domain driver's power state, stored in the driver. pub(crate) enum PowerManagement<'d, M: RetentionMemory> { /// Active, not retained: the held lock keeps `TOP` powered. PowerDomainLock { _lock: PowerDomainLock }, - /// Retained: `node` points into the caller-owned memory borrowed for `'d`. + /// Retained: `node` points into the caller-owned memory. Retain { node: NonNull, _mem: PhantomData<&'d mut M>, @@ -502,17 +636,11 @@ impl<'d, M: RetentionMemory> PowerManagement<'d, M> { impl Drop for PowerManagement<'_, M> { fn drop(&mut self) { if let Self::Retain { node, .. } = self { - // SAFETY: the node lives in caller memory borrowed for `'d`, which - // outlives `self`, so it is still valid to unlink here. unsafe { deregister_node(node.as_mut()) }; } } } -// SAFETY: the only thread-unsafe state a `Retain` holds is the raw node/link -// pointers, and those are only ever dereferenced on the single HP core under the -// `REGISTRY` mutex (see `arm_link`/`deregister_node`); the owner never follows -// them otherwise. unsafe impl Send for PowerManagement<'_, M> {} unsafe impl Sync for PowerManagement<'_, M> {} @@ -536,39 +664,43 @@ impl defmt::Format for PowerManagement<'_, M> { } /// One step of a chip's TOP-domain retention program (`chip::OPS`), expanded -/// into PAU regDMA nodes by [`sys_periph::build_link`]. `Write`/`Wait` are -/// restore-only (they re-apply state the backup can't capture, e.g. unlocking -/// TEE/APM or pulsing a clock-update bit). -// A given chip may not use every variant (only the H2 needs `Wait`). -#[allow(dead_code)] +/// into PAU regDMA nodes by [`sys_periph::build_link`]. #[derive(Clone, Copy)] enum SysOp { - /// Back up/restore `count` consecutive registers starting at `base`. - Continuous { base: u32, count: u32 }, - /// Back up `count` words from `backup`, restore them to `restore` (e.g. - /// GPIO output-enable via the W1TS register). + /// Back up/restore `count` consecutive registers starting at `addr`. + Continuous { addr: fn() -> u32, count: u32 }, + /// Back up `count` consecutive registers starting at `backup`, restore into + /// `restore`. ContinuousSplit { - backup: u32, - restore: u32, + backup: fn() -> u32, + restore: fn() -> u32, count: u32, }, /// Restore-only masked write of `value` to `addr`. - Write { addr: u32, value: u32, mask: u32 }, + Write { + addr: fn() -> u32, + value: u32, + mask: u32, + }, /// Restore-only poll of `addr` until `(reg & mask) == value`. - Wait { addr: u32, value: u32, mask: u32 }, - /// The shared console-UART config sequence for the UART based at `base`. - Uart { base: u32 }, - /// The shared SysTimer save/restore sequence based at `base`. - Systimer { base: u32 }, + #[cfg(sleep_regdma_wait_ops)] + Wait { + addr: fn() -> u32, + value: u32, + mask: u32, + }, + /// The shared console-UART config sequence. + Uart { base: fn() -> u32 }, + /// The shared SysTimer save/restore sequence. + Systimer { base: fn() -> u32 }, } /// PAU nodes emitted for one [`SysOp`]. const fn op_nodes(op: &SysOp) -> usize { match op { - SysOp::Continuous { .. } - | SysOp::ContinuousSplit { .. } - | SysOp::Write { .. } - | SysOp::Wait { .. } => 1, + SysOp::Continuous { .. } | SysOp::ContinuousSplit { .. } | SysOp::Write { .. } => 1, + #[cfg(sleep_regdma_wait_ops)] + SysOp::Wait { .. } => 1, SysOp::Uart { .. } => UART_NODE_COUNT, SysOp::Systimer { .. } => SYSTIMER_NODE_COUNT, } @@ -578,9 +710,11 @@ const fn op_nodes(op: &SysOp) -> usize { const fn op_words(op: &SysOp) -> usize { match op { SysOp::Continuous { count, .. } | SysOp::ContinuousSplit { count, .. } => *count as usize, - SysOp::Uart { .. } => UART_RETENTION_REGS_CNT as usize, + SysOp::Uart { .. } => UART_WORDS, SysOp::Systimer { .. } => SYSTIMER_CONT_WORDS, - SysOp::Write { .. } | SysOp::Wait { .. } => 0, + SysOp::Write { .. } => 0, + #[cfg(sleep_regdma_wait_ops)] + SysOp::Wait { .. } => 0, } } @@ -606,47 +740,39 @@ const fn ops_buf_words(ops: &[SysOp]) -> usize { w } -// SysTimer register offsets and bit masks (shared; only the base differs per -// chip). Offsets/masks from ESP-IDF v5.4 `systimer_reg.h`; the save/restore -// sequence mirrors `systimer_regs_retention[]`. -const ST_UNIT_UPDATE: u32 = 1 << 30; -const ST_UNIT_VALUE_VALID: u32 = 1 << 29; +const ST_UNIT_OP_UPDATE: u32 = 1 << 30; +const ST_UNIT_OP_VALUE_VALID: u32 = 1 << 29; const ST_UNIT_LOAD: u32 = 1 << 0; const ST_COMP_LOAD: u32 = 1 << 0; -const ST_TARGET_PERIOD_MODE: u32 = 1 << 30; -/// TARGET0_HI ..= TARGET2_CONF, i.e. all three targets' hi/lo/conf. -const ST_TARGETS_LEN: u32 = 9; -/// One node per SysTimer step of `build_systimer_seq`. -const SYSTIMER_NODE_COUNT: usize = 19; -/// SysTimer CONTINUOUS words: unit0/1 value (2+2), targets (9), conf, int_ena. +const ST_TARGET_CONF_PERIOD_MODE: u32 = 1 << 30; +const ST_TARGETS_LEN: u32 = span!(SYSTIMER, [trgt(0).hi()]..=[target_conf(2)]); +// Nodes `build_systimer_seq` emits: four per counter unit (latch, poll, read, +// load), the target values, one load per comparator, a clear and set to re-arm +// period mode on target0/1 plus a clear on target2, then `conf` and `int_ena`. +const SYSTIMER_NODE_COUNT: usize = 2 * 4 + 1 + 3 + 2 * 2 + 1 + 1 + 1; +// SysTimer CONTINUOUS words: unit0/1 value (2+2), the targets, conf, int_ena. const SYSTIMER_CONT_WORDS: usize = 2 + 2 + ST_TARGETS_LEN as usize + 1 + 1; /// Build the SysTimer retention sequence for the timer at `base` into `nodes`, -/// drawing its RAM from `buf_base` starting at word `start_word`. Fills exactly -/// [`SYSTIMER_NODE_COUNT`] nodes and consumes [`SYSTIMER_CONT_WORDS`] words. -/// -/// Backup latches each unit's counter (UPDATE + wait for VALUE_VALID) and reads -/// it; restore loads it back and triggers a load. The value is read from -/// VALUE_HI/LO but restored into LOAD_HI/LO, hence the split backup/restore -/// addresses. +/// drawing its RAM from `buf_base`. fn build_systimer_seq(base: u32, nodes: &mut [RegdmaLink], buf_base: *mut u32, start_word: usize) { - let st_conf = base; - let st_unit0_op = base + 0x04; - let st_unit1_op = base + 0x08; - let st_unit0_load_hi = base + 0x0C; - let st_unit1_load_hi = base + 0x14; - let st_target0_hi = base + 0x1C; - let st_target0_conf = base + 0x34; - let st_target1_conf = base + 0x38; - let st_target2_conf = base + 0x3C; - let st_unit0_value_hi = base + 0x40; - let st_unit1_value_hi = base + 0x48; - let st_comp0_load = base + 0x50; - let st_comp1_load = base + 0x54; - let st_comp2_load = base + 0x58; - let st_unit0_load = base + 0x5C; - let st_unit1_load = base + 0x60; - let st_int_ena = base + 0x64; + let st_conf = base + reg_off!(SYSTIMER, conf()); + let st_unit0_op = base + reg_off!(SYSTIMER, unit_op(0)); + let st_unit1_op = base + reg_off!(SYSTIMER, unit_op(1)); + let st_unit0_load_hi = base + reg_off!(SYSTIMER, unitload(0).hi()); + let st_unit1_load_hi = base + reg_off!(SYSTIMER, unitload(1).hi()); + let st_target0_hi = base + reg_off!(SYSTIMER, trgt(0).hi()); + let st_target0_conf = base + reg_off!(SYSTIMER, target_conf(0)); + let st_target1_conf = base + reg_off!(SYSTIMER, target_conf(1)); + let st_target2_conf = base + reg_off!(SYSTIMER, target_conf(2)); + let st_unit0_value_hi = base + reg_off!(SYSTIMER, unit_value(0).hi()); + let st_unit1_value_hi = base + reg_off!(SYSTIMER, unit_value(1).hi()); + let st_comp0_load = base + reg_off!(SYSTIMER, comp_load(0)); + let st_comp1_load = base + reg_off!(SYSTIMER, comp_load(1)); + let st_comp2_load = base + reg_off!(SYSTIMER, comp_load(2)); + let st_unit0_load = base + reg_off!(SYSTIMER, unit_load(0)); + let st_unit1_load = base + reg_off!(SYSTIMER, unit_load(1)); + let st_int_ena = base + reg_off!(SYSTIMER, int_ena()); let mut word = start_word; let mut alloc = |len: u32| -> u32 { @@ -672,9 +798,15 @@ fn build_systimer_seq(base: u32, nodes: &mut [RegdmaLink], buf_base: *mut u32, s st_unit1_load, ), ] { - nodes[node] = RegdmaLink::write(op, ST_UNIT_UPDATE, ST_UNIT_UPDATE, false, true); + nodes[node] = RegdmaLink::write(op, ST_UNIT_OP_UPDATE, ST_UNIT_OP_UPDATE, false, true); node += 1; - nodes[node] = RegdmaLink::wait(op, ST_UNIT_VALUE_VALID, ST_UNIT_VALUE_VALID, false, true); + nodes[node] = RegdmaLink::wait( + op, + ST_UNIT_OP_VALUE_VALID, + ST_UNIT_OP_VALUE_VALID, + false, + true, + ); node += 1; let mem = alloc(2); nodes[node] = RegdmaLink::continuous_split(value_hi, load_hi, mem, 2); @@ -693,18 +825,18 @@ fn build_systimer_seq(base: u32, nodes: &mut [RegdmaLink], buf_base: *mut u32, s } // Re-arm period mode: clear+set for target0/1, clear for target2. for target in [st_target0_conf, st_target1_conf] { - nodes[node] = RegdmaLink::write(target, 0, ST_TARGET_PERIOD_MODE, true, false); + nodes[node] = RegdmaLink::write(target, 0, ST_TARGET_CONF_PERIOD_MODE, true, false); node += 1; nodes[node] = RegdmaLink::write( target, - ST_TARGET_PERIOD_MODE, - ST_TARGET_PERIOD_MODE, + ST_TARGET_CONF_PERIOD_MODE, + ST_TARGET_CONF_PERIOD_MODE, true, false, ); node += 1; } - nodes[node] = RegdmaLink::write(st_target2_conf, 0, ST_TARGET_PERIOD_MODE, true, false); + nodes[node] = RegdmaLink::write(st_target2_conf, 0, ST_TARGET_CONF_PERIOD_MODE, true, false); node += 1; // Work-enable and interrupt-enable state. @@ -719,13 +851,7 @@ fn build_systimer_seq(base: u32, nodes: &mut [RegdmaLink], buf_base: *mut u32, s debug_assert!(word - start_word == SYSTIMER_CONT_WORDS); } -/// Caller-owned backing store for the TOP-domain system-peripheral register set -/// (PCR, interrupt matrix, HP system, TEE/APM, IO MUX, GPIO matrix, flash SPI -/// mem, console UART and SysTimer - see the chip's `OPS` retention program). -/// -/// The core state regDMA must retain for the `TOP` domain to power down at all; -/// the caller opts in via [`RtcSleepConfig::with_top_power_down`]. Individual -/// peripherals opt into retaining their own config via `with_retention_memory`. +/// Caller-owned backing store for the TOP-domain system-peripheral register set. /// /// [`RtcSleepConfig::with_top_power_down`]: crate::rtc_cntl::sleep::RtcSleepConfig::with_top_power_down #[instability::unstable] @@ -757,8 +883,8 @@ impl SystemRetentionMemory { /// Enable the regDMA bus clock and release its reset (`pau_ll_enable_bus_clock`), /// and bound the WAIT polling so a never-satisfied condition can't hang the -/// engine (`pau_hal_set_regdma_wait_timeout`, ESP-IDF `PAU_REGDMA_LINK_WAIT_*`). -// `#[ram]`: also called from the wake path (see `restore_top_retention`). +/// engine. +// `pau_hal_set_regdma_wait_timeout`, ESP-IDF `PAU_REGDMA_LINK_WAIT_*` #[ram] fn regdma_clock_and_timeout() { PCR::regs().regdma_conf().modify(|_, w| { @@ -772,9 +898,7 @@ fn regdma_clock_and_timeout() { } /// Software-trigger a regDMA transfer of system link 0 and wait for it. `backup` -/// copies registers into RAM, else restores RAM back into registers. Used on -/// [`chip::SW_TRIGGER_REGDMA`] chips whose PAU powers off with `TOP`, so the PMU -/// can't drive the transfer. +/// copies registers into RAM, else restores RAM back into registers. // Mirrors ESP-IDF `pau_hal_start_regdma_system_link`. // `#[ram]`: the restore runs on wake before the flash SPI controller is back. #[ram] @@ -803,11 +927,6 @@ fn sw_trigger_system_link(backup: bool) { /// sleep: program PAU entry link 0 and start the backup. Must be called after /// the PMU power config (which resets the backup-enable bits) and before the /// sleep request; rebuilds the chain from the live registry each time. -/// -/// Where the PAU survives the `TOP` power-down the PMU drives backup/restore in -/// hardware. On [`chip::SW_TRIGGER_REGDMA`] chips the PAU powers off with `TOP`, -/// so the hardware backup is disabled and the backup is triggered in software -/// here, with [`restore_top_retention`] doing the restore on wake. fn enable_top_retention(mem: &mut SystemRetentionMemory) { regdma_clock_and_timeout(); @@ -818,7 +937,7 @@ fn enable_top_retention(mem: &mut SystemRetentionMemory) { .write(|w| unsafe { w.bits(head) }); let pmu = PMU::regs(); - if chip::SW_TRIGGER_REGDMA { + if cfg!(sleep_regdma_sw_trigger) { // The PAU powers off with TOP: the PMU can't run the backup on the // active->sleep transition, so disable it and back up in software now. pmu.hp_sleep_backup() @@ -836,7 +955,7 @@ fn enable_top_retention(mem: &mut SystemRetentionMemory) { } } -/// Restore the TOP-domain peripherals on wake for [`chip::SW_TRIGGER_REGDMA`] +/// Restore the TOP-domain peripherals on wake for `sleep_regdma_sw_trigger` /// chips, whose PAU lost its own configuration with the `TOP` power-down. /// /// Re-enables the regDMA bus clock, re-programs entry link 0 (the linked list in @@ -846,7 +965,7 @@ fn enable_top_retention(mem: &mut SystemRetentionMemory) { // `#[ram]`: runs on the wake path before the flash SPI controller is restored. #[ram] pub(crate) fn restore_top_retention(mem: &mut SystemRetentionMemory) { - if !chip::SW_TRIGGER_REGDMA { + if !cfg!(sleep_regdma_sw_trigger) { return; } regdma_clock_and_timeout(); @@ -888,9 +1007,6 @@ pub(crate) fn disable_timg0_flashboot_wdt() { /// `RtcSleepConfig`. Holds the caller's opt-in choices and retention memory plus /// the chip-agnostic resolve/enter logic; a chip only maps the resolved decision /// onto its own `PowerDownFlags`. -/// -/// Raw pointers (not borrows) keep the embedding `RtcSleepConfig` `Copy`; the -/// setters take `&'static mut`. #[derive(Clone, Copy)] pub(crate) struct SleepRetention { cpu_power_down: bool, @@ -936,9 +1052,7 @@ impl SleepRetention { } /// Resolve which domains may actually power down for a light sleep, given the - /// opt-in choices, caller memory and active power-domain locks. Returns - /// `(cpu_pd, top_pd)`; `top_pd` implies `cpu_pd`, and a domain only powers - /// down with its retention memory and no lock (else it clock-gates). + /// opt-in choices, caller memory and active power-domain locks. pub(crate) fn resolve(&self) -> (bool, bool) { let have_cpu = !self.cpu_mem.is_null(); let have_sys = !self.top_mem.is_null(); @@ -980,8 +1094,8 @@ impl SleepRetention { } } -/// Chip-agnostic interpreter that expands a chip's `OPS` program into PAU regDMA -/// nodes (in retention-priority order, system clock first). +// Chip-agnostic interpreter that expands a chip's `OPS` program into PAU regDMA +// nodes (in retention-priority order, system clock first). // Mirrors ESP-IDF's `SLEEP_RETENTION_MODULE_SYS_PERIPH` + `..._CLOCK_SYSTEM` // (`soc//system_retention_periph.c`, `.../sleep_clock.c`). mod sys_periph { @@ -991,9 +1105,10 @@ mod sys_periph { SYSTIMER_NODE_COUNT, SysOp, UART_NODE_COUNT, - UART_RETENTION_REGS_CNT, + UART_OPS, + UART_WORDS, + build_periph_seq, build_systimer_seq, - build_uart_seq, chip, }; @@ -1026,9 +1141,9 @@ mod sys_periph { for op in chip::OPS { match *op { - SysOp::Continuous { base, count } => { + SysOp::Continuous { addr, count } => { let mem = unsafe { buf_base.add(word) } as u32; - nodes[node] = RegdmaLink::continuous(base, mem, count); + nodes[node] = RegdmaLink::continuous(addr(), mem, count); word += count as usize; node += 1; } @@ -1038,27 +1153,33 @@ mod sys_periph { count, } => { let mem = unsafe { buf_base.add(word) } as u32; - nodes[node] = RegdmaLink::continuous_split(backup, restore, mem, count); + nodes[node] = RegdmaLink::continuous_split(backup(), restore(), mem, count); word += count as usize; node += 1; } SysOp::Write { addr, value, mask } => { - nodes[node] = RegdmaLink::write(addr, value, mask, true, false); + nodes[node] = RegdmaLink::write(addr(), value, mask, true, false); node += 1; } + #[cfg(sleep_regdma_wait_ops)] SysOp::Wait { addr, value, mask } => { - nodes[node] = RegdmaLink::wait(addr, value, mask, true, false); + nodes[node] = RegdmaLink::wait(addr(), value, mask, true, false); node += 1; } SysOp::Uart { base } => { - let mem = unsafe { buf_base.add(word) } as u32; - build_uart_seq(base, &mut nodes[node..node + UART_NODE_COUNT], mem); - word += UART_RETENTION_REGS_CNT as usize; + let mem = unsafe { buf_base.add(word) }; + build_periph_seq( + base(), + UART_OPS, + &mut nodes[node..node + UART_NODE_COUNT], + mem, + ); + word += UART_WORDS; node += UART_NODE_COUNT; } SysOp::Systimer { base } => { build_systimer_seq( - base, + base(), &mut nodes[node..node + SYSTIMER_NODE_COUNT], buf_base, word, diff --git a/esp-hal/src/rtc_cntl/retention/esp32c6.rs b/esp-hal/src/rtc_cntl/retention/esp32c6.rs index 5ca4beaab41..88cab6f73d5 100644 --- a/esp-hal/src/rtc_cntl/retention/esp32c6.rs +++ b/esp-hal/src/rtc_cntl/retention/esp32c6.rs @@ -1,155 +1,110 @@ //! ESP32-C6 register data for TOP-domain regDMA retention and CPU-domain //! software retention. -//! -//! Pure data consumed by the chip-agnostic logic in `retention` and -//! `cpu_retention`. Region sizes note the sizing end -//! register (`count = ((end - base) / 4) + 1`). // References (ESP-IDF `v5.4`): `soc/esp32c6/system_retention_periph.c`, // `esp_hw_support/.../esp32c6/sleep_clock.c`, `.../esp32c6/sleep_cpu.c`. -use super::SysOp::{self, Continuous, ContinuousSplit, Systimer, Uart, Write}; +use super::{PeriphOp, Region, SysOp}; /// The TOP-domain SYS_PERIPH retention program, in retention-priority order /// (system clock first). Interpreted by `retention::sys_periph::build_link`. pub(super) const OPS: &[SysOp] = &[ - // PRI_0: system clock/reset (PCR). - Continuous { - base: 0x6009_6000, - count: 79, - }, // PCR ..= PCR_SRAM_POWER_CONF_REG (+0x138) - Continuous { - base: 0x6009_6FF0, - count: 1, - }, // PCR_RESET_EVENT_BYPASS_REG - // PRI_2: unlock TEE/APM (clear TEE_M4_MODE_CTRL) before restoring them. - Write { - addr: 0x6009_8010, - value: 0, - mask: 0xFFFF_FFFF, - }, // TEE_M4_MODE_CTRL_REG + // PRI_0: system clock/reset. + continuous!(PCR, [uart(0).conf()]..=[sram_power_conf()]), + continuous!(PCR, [reset_event_bypass()], 1), + // PRI_2: unlock TEE/APM before restoring them. + write_reg!(TEE, [m_mode_ctrl(4)], 0, 0xFFFF_FFFF), // PRI_4/5: TEE/APM, interrupt matrix, HP system. - Continuous { - base: 0x6009_9000, - count: 68, - }, // HP_APM ..= HP_APM_CLOCK_GATE_REG (+0x10c) - Continuous { - base: 0x6009_8000, - count: 33, - }, // TEE ..= TEE_CLOCK_GATE_REG (+0x80) - Continuous { - base: 0x6001_0000, - count: 81, - }, // INTMTX ..= INTMTX_CORE0_CLOCK_GATE_REG (+0x140) - Continuous { - base: 0x6009_5000, - count: 18, - }, // HP_SYSTEM ..= HP_SYSTEM_MEM_TEST_CONF_REG (+0x44) + continuous!(HP_APM, [region_filter_en()]..=[clock_gate()]), + continuous!(TEE, [m_mode_ctrl(0)]..=[clock_gate()]), + continuous!(INTERRUPT_CORE0, [core_0_intr_map(0)]..=[clock_gate()]), + continuous!( + HP_SYS, + [external_device_encrypt_decrypt_control()]..=[mem_test_conf()] + ), // PRI_5: console UART0. - Uart { base: 0x6000_0000 }, + uart_seq!(UART0), // PRI_6: IO MUX + GPIO matrix. - Continuous { - base: 0x6009_0000, - count: 32, - }, // IO_MUX ..= IO_MUX_GPIO30_REG (+0x7c) - Continuous { - base: 0x6009_1554, - count: 35, - }, // GPIO_FUNC0_OUT_SEL ..= GPIO_FUNC34_OUT_SEL - Continuous { - base: 0x6009_114C, - count: 127, - }, // GPIO_STATUS_NEXT ..= GPIO_FUNC124_IN_SEL - Continuous { - base: 0x6009_1000, - count: 64, - }, // GPIO ..= GPIO_PIN34_REG (+0xfc) - ContinuousSplit { - backup: 0x6009_1020, - restore: 0x6009_1024, - count: 1, - }, // GPIO_ENABLE_REG .. GPIO_ENABLE_W1TS_REG - ContinuousSplit { - backup: 0x6009_102c, - restore: 0x6009_1030, - count: 1, - }, // GPIO_ENABLE1_REG .. GPIO_ENABLE1_W1TS_REG (pins 32..=34) - // PRI_6: Flash SPI mem (SPIMEM1 then SPIMEM0). MMU content/index registers - // are intentionally excluded (see ESP-IDF note). - Continuous { - base: 0x6000_3000, - count: 55, - }, // SPIMEM1 ..= SPI_MEM_SPI_SMEM_DDR (+0xd8) - Continuous { - base: 0x6000_3100, - count: 41, - }, // SPIMEM1 FMEM_PMS0_ATTR ..= SMEM_AC (+0x1a0) - Continuous { - base: 0x6000_3200, - count: 1, - }, // SPIMEM1 CLOCK_GATE - Continuous { - base: 0x6000_3384, - count: 31, - }, // SPIMEM1 MMU_POWER_CTRL ..= DATE (+0x3fc) - Continuous { - base: 0x6000_2000, - count: 55, - }, // SPIMEM0 ..= SPI_MEM_SPI_SMEM_DDR - Continuous { - base: 0x6000_2100, - count: 41, - }, // SPIMEM0 FMEM_PMS0_ATTR ..= SMEM_AC - Continuous { - base: 0x6000_2200, - count: 1, - }, // SPIMEM0 CLOCK_GATE - Continuous { - base: 0x6000_2384, - count: 31, - }, // SPIMEM0 MMU_POWER_CTRL ..= DATE + continuous!(IO_MUX, [pin_ctrl()]..=[gpio(30)]), + continuous!(GPIO, [func_out_sel_cfg(0)], 35), + continuous!(GPIO, [status_next()]..=[func_in_sel_cfg(124)]), + continuous!(GPIO, [bt_select()]..=[pin(34)]), + continuous_split!(GPIO, [enable()] => [enable_w1ts()], 1), + // PRI_6: flash SPI mem, SPIMEM1 (SPI1) before SPIMEM0 (SPI0). The MMU + // content/index registers are intentionally excluded, + // which is why each controller's last window starts at MMU_POWER_CTRL. + continuous!(SPI1, [cmd()]..=[spi_smem_ddr()]), + continuous!(SPI1, [spi_fmem_pms_attr(0)]..=[spi_smem_ac()]), + continuous!(SPI1, [clock_gate()], 1), + continuous!(SPI1, [mmu_power_ctrl()]..=[date()]), + continuous!(SPI0, [cmd()]..=[spi_smem_ddr()]), + continuous!(SPI0, [spi_fmem_pms_attr(0)]..=[spi_smem_ac()]), + continuous!(SPI0, [clock_gate()], 1), + continuous!(SPI0, [mmu_power_ctrl()]..=[date()]), // PRI_6: SysTimer. - Systimer { base: 0x6000_A000 }, + systimer_seq!(SYSTIMER), ]; -/// The C6's PAU survives the `TOP` power-down (it is in `HP_AON`), so the PMU -/// drives the regDMA backup/restore in hardware on the sleep/wake transition. -pub(super) const SW_TRIGGER_REGDMA: bool = false; - -// Opt-in peripheral config-register retention data (offsets/masks/maps/counts), -// consumed by the chip-agnostic sequence builders in `retention`. -// // UART: ESP-IDF v5.4 `uart_periph.c` `UART_SLEEP_RETENTION_ENTRIES`, `uart_reg.h`. -pub(super) const UART_INT_ENA_OFF: u32 = 0x0C; // UART_INT_ENA_REG: ADDR_MAP window base -pub(super) const UART_REG_UPDATE_OFF: u32 = 0x98; // UART_REG_UPDATE_REG -pub(super) const UART_REG_UPDATE: u32 = 1 << 0; -/// Registers retained (set bits in [`UART_REGS_MAP`]). -pub(super) const UART_RETENTION_REGS_CNT: u32 = 21; -/// `uart_regs_map[4]`: config registers in the INT_ENA..ID window. -pub(super) const UART_REGS_MAP: [u32; 4] = [0x007f_ff6d, 0x0000_0010, 0, 0]; +pub(super) const UART_OPS: &[PeriphOp] = &[ + periph_continuous!(UART0, [int_ena()]), + periph_continuous!(UART0, [clkdiv()]..=[rx_filt()]), + periph_continuous!(UART0, [conf0()]..=[conf1()]), + periph_continuous!(UART0, [hwfc_conf()]..=[tout_conf()]), + periph_continuous!(UART0, [id()]), + // Restore-only: pulse REG_UPDATE to latch the shadow (`_SYNC`) registers. + periph_write!(UART0, [reg_update()], 1 << 0, 1 << 0), + periph_wait!(UART0, [reg_update()], 0, 1 << 0), +]; // I2C: ESP-IDF v5.4 `i2c_periph.c` `i2c0_regs_retention`, `i2c_reg.h`. -pub(super) const I2C_SCL_LOW_PERIOD_OFF: u32 = 0x00; // I2C_SCL_LOW_PERIOD_REG: ADDR_MAP window base -pub(super) const I2C_CTR_OFF: u32 = 0x04; // I2C_CTR_REG -pub(super) const I2C_FSM_RST: u32 = 1 << 10; // I2C_FSM_RST (value == mask) -pub(super) const I2C_CONF_UPGATE: u32 = 1 << 11; // I2C_CONF_UPGATE (value == mask) -/// Registers retained (set bits in [`I2C_REGS_MAP`]). -pub(super) const I2C_RETENTION_REGS_CNT: u32 = 18; -/// `i2c0_regs_map[4]`: config registers in the `SCL_LOW_PERIOD..SCL_STRETCH_CONF` window. -pub(super) const I2C_REGS_MAP: [u32; 4] = [0xc03f_345b, 0x3, 0, 0]; +pub(super) const I2C_OPS: &[PeriphOp] = &[ + periph_continuous!(I2C0, [scl_low_period()]..=[ctr()]), + periph_continuous!(I2C0, [to()]..=[slave_addr()]), + periph_continuous!(I2C0, [fifo_conf()]), + periph_continuous!(I2C0, [int_ena()]), + periph_continuous!(I2C0, [sda_hold()]..=[sda_sample()]), + periph_continuous!(I2C0, [scl_start_hold()]..=[clk_conf()]), + periph_continuous!(I2C0, [scl_st_time_out()]..=[scl_stretch_conf()]), + // Restore-only: pulse FSM reset, request a config update, wait for it to latch. + periph_write!(I2C0, [ctr()], 1 << 10, 1 << 10), // I2C_FSM_RST + periph_write!(I2C0, [ctr()], 0, 1 << 10), + periph_write!(I2C0, [ctr()], 1 << 11, 1 << 11), // I2C_CONF_UPGATE + periph_wait!(I2C0, [ctr()], 0, 1 << 11), +]; -// GPSPI2: ESP-IDF v5.4 `spi_periph.c` `spi2_regs_retention`, `spi_reg.h`. -pub(super) const SPI_CMD_OFF: u32 = 0x00; // SPI_CMD_REG: ADDR_MAP window base -/// Registers retained (set bits in [`SPI_REGS_MAP`]). -pub(super) const SPI_RETENTION_REGS_CNT: u32 = 12; -/// `spi_regs_map[4]`: config registers in the `CMD..SLAVE` window. -pub(super) const SPI_REGS_MAP: [u32; 4] = [0x0000_31ff, 0x0100_0000, 0, 0]; +// GPSPI2: ESP-IDF v5.4 `spi_periph.c` `spi2_regs_retention`, `spi_reg.h`. The +// config registers are only reachable while the SPI function clock runs; +// `Spi::with_retention_memory` holds that clock across the retention lifetime. +pub(super) const SPI_OPS: &[PeriphOp] = &[ + periph_continuous!(SPI2, [cmd()]..=[misc()]), + periph_continuous!(SPI2, [dma_conf()]..=[dma_int_ena()]), + periph_continuous!(SPI2, [slave()]), +]; + +// Interrupt matrix priorities. +pub(crate) const INTPRI_REGIONS: [Region; 2] = [ + region!(INTPRI, [cpu_int_enable()]..=[rnd_eco_low()]), + region!(INTPRI, [rnd_eco_high()]), +]; -// CPU-domain device-register bases lost when `pd_cpu` powers down (consumed by -// `cpu_retention`; the region layout around them is shared). -pub(crate) const INTPRI_BASE: u32 = 0x600C_5000; // interrupt priority (INTPRI) -pub(crate) const CACHE_BASE: u32 = 0x600C_8000; // L1 cache control (EXTMEM/CACHE) -pub(crate) const PLIC_MX_BASE: u32 = 0x2000_1000; // PLIC machine interrupts -pub(crate) const PLIC_UX_BASE: u32 = 0x2000_1400; // PLIC user interrupts -pub(crate) const CLINT_MINT_BASE: u32 = 0x2000_1800; // CLINT machine timer -pub(crate) const CLINT_UINT_BASE: u32 = 0x2000_1C00; // CLINT user timer +// L1 cache control. +pub(crate) const CACHE_REGIONS: [Region; 2] = [ + region!(EXTMEM, [l1_cache_ctrl()]), + region!(EXTMEM, [l1_cache_wrap_around_ctrl()]), +]; + +// PLIC machine/user interrupt controllers. +pub(crate) const PLIC_REGIONS: [Region; 4] = [ + region!(PLIC_MX, [mxint_enable()]..=[mxint_claim()]), + region!(PLIC_MX, [mxint_conf()]), + region!(PLIC_UX, [uxint_enable()]..=[uxint_claim()]), + region!(PLIC_UX, [uxint_conf()]), +]; + +// CLINT machine/user timers. The comparators are 64-bit, which `span!` accounts +// for. +pub(crate) const CLINT_REGIONS: [Region; 2] = [ + region!(CLINT, [msip()]..=[mtimecmp()]), + region!(CLINT, [usip()]..=[utimecmp()]), +]; diff --git a/esp-hal/src/rtc_cntl/retention/esp32h2.rs b/esp-hal/src/rtc_cntl/retention/esp32h2.rs index 1bdbf437e75..8c49c9751fe 100644 --- a/esp-hal/src/rtc_cntl/retention/esp32h2.rs +++ b/esp-hal/src/rtc_cntl/retention/esp32h2.rs @@ -1,160 +1,113 @@ //! ESP32-H2 register data for TOP-domain regDMA retention and CPU-domain //! software retention. -//! -//! Pure data consumed by the chip-agnostic logic in `retention` and -//! `cpu_retention`. Region sizes note the sizing end register -//! (`count = ((end - base) / 4) + 1`). // References (ESP-IDF `v5.4`): `soc/esp32h2/system_retention_periph.c`, // `esp_hw_support/.../esp32h2/sleep_clock.c`, `.../esp32h2/sleep_cpu.c`. -use super::SysOp::{self, Continuous, ContinuousSplit, Systimer, Uart, Wait, Write}; +use super::{PeriphOp, Region, SysOp}; /// The TOP-domain SYS_PERIPH retention program, in retention-priority order /// (system clock first). Interpreted by `retention::sys_periph::build_link`. pub(super) const OPS: &[SysOp] = &[ - // PRI_0: system clock/reset (PCR). The H2 must also pulse the bus-clock - // update bit on restore for the new clock config to take effect. - Continuous { - base: 0x6009_6000, - count: 85, - }, // PCR ..= PCR_PWDET_SAR_CLK_CONF_REG (+0x150) - Continuous { - base: 0x6009_6FF0, - count: 1, - }, // PCR_RESET_EVENT_BYPASS_REG - Write { - addr: 0x6009_6148, - value: 0x1, - mask: 0x1, - }, // PCR_BUS_CLK_UPDATE (BUS_CLOCK_UPDATE) - Wait { - addr: 0x6009_6148, - value: 0, - mask: 0x1, - }, // wait for it to self-clear - // PRI_2: unlock TEE/APM (clear TEE_M4_MODE_CTRL) before restoring them. - Write { - addr: 0x6009_8010, - value: 0, - mask: 0xFFFF_FFFF, - }, // TEE_M4_MODE_CTRL_REG + // PRI_0: system clock/reset. The bus-clock update bit has to be pulsed on + // restore for the restored clock config to take effect. + continuous!(PCR, [uart(0).conf()]..=[pwdet_sar_clk_conf()]), + continuous!(PCR, [reset_event_bypass()], 1), + write_reg!(PCR, [bus_clk_update()], 0x1, 0x1), + wait_reg!(PCR, [bus_clk_update()], 0, 0x1), // wait for it to self-clear + // PRI_2: unlock TEE/APM before restoring them. + write_reg!(TEE, [m_mode_ctrl(4)], 0, 0xFFFF_FFFF), // PRI_4/5: TEE/APM, interrupt matrix, HP system. - Continuous { - base: 0x6009_9000, - count: 68, - }, // HP_APM ..= HP_APM_CLOCK_GATE_REG (+0x10c) - Continuous { - base: 0x6009_8000, - count: 33, - }, // TEE ..= TEE_CLOCK_GATE_REG (+0x80) - Continuous { - base: 0x6001_0000, - count: 69, - }, // INTMTX ..= INTMTX_CORE0_CLOCK_GATE_REG (+0x110) - Continuous { - base: 0x6009_5000, - count: 12, - }, // HP_SYSTEM ..= HP_SYSTEM_MEM_TEST_CONF_REG (+0x2c) + continuous!(HP_APM, [region_filter_en()]..=[clock_gate()]), + continuous!(TEE, [m_mode_ctrl(0)]..=[clock_gate()]), + continuous!(INTERRUPT_CORE0, [core_0_intr_map(0)]..=[clock_gate()]), + continuous!( + HP_SYS, + [external_device_encrypt_decrypt_control()]..=[mem_test_conf()] + ), // PRI_5: console UART0. - Uart { base: 0x6000_0000 }, - // PRI_6: IO MUX + GPIO matrix (fewer pins than the C6). - Continuous { - base: 0x6009_0000, - count: 29, - }, // IO_MUX ..= IO_MUX_GPIO27_REG (+0x70) - Continuous { - base: 0x6009_1554, - count: 32, - }, // GPIO_FUNC0_OUT_SEL ..= GPIO_FUNC31_OUT_SEL - Continuous { - base: 0x6009_114C, - count: 127, - }, // GPIO_STATUS_NEXT ..= GPIO_FUNC124_IN_SEL - Continuous { - base: 0x6009_1000, - count: 61, - }, // GPIO ..= GPIO_PIN31_REG (+0xf0) - ContinuousSplit { - backup: 0x6009_1020, - restore: 0x6009_1024, - count: 1, - }, // GPIO_ENABLE_REG .. GPIO_ENABLE_W1TS_REG - // PRI_6: Flash SPI mem (SPIMEM1 then SPIMEM0), identical layout to the C6. - // MMU content/index registers are intentionally excluded (see ESP-IDF note). - Continuous { - base: 0x6000_3000, - count: 55, - }, // SPIMEM1 ..= SPI_MEM_SPI_SMEM_DDR (+0xd8) - Continuous { - base: 0x6000_3100, - count: 41, - }, // SPIMEM1 FMEM_PMS0_ATTR ..= SMEM_AC (+0x1a0) - Continuous { - base: 0x6000_3200, - count: 1, - }, // SPIMEM1 CLOCK_GATE - Continuous { - base: 0x6000_3384, - count: 31, - }, // SPIMEM1 MMU_POWER_CTRL ..= DATE (+0x3fc) - Continuous { - base: 0x6000_2000, - count: 55, - }, // SPIMEM0 ..= SPI_MEM_SPI_SMEM_DDR - Continuous { - base: 0x6000_2100, - count: 41, - }, // SPIMEM0 FMEM_PMS0_ATTR ..= SMEM_AC - Continuous { - base: 0x6000_2200, - count: 1, - }, // SPIMEM0 CLOCK_GATE - Continuous { - base: 0x6000_2384, - count: 31, - }, // SPIMEM0 MMU_POWER_CTRL ..= DATE - // PRI_6: SysTimer (base differs from the C6). - Systimer { base: 0x6000_B000 }, + uart_seq!(UART0), + // PRI_6: IO MUX + GPIO matrix. + continuous!(IO_MUX, [pin_ctrl()]..=[gpio(27)]), + continuous!(GPIO, [func_out_sel_cfg(0)], 32), + continuous!(GPIO, [status_next()]..=[func_in_sel_cfg(124)]), + continuous!(GPIO, [bt_select()]..=[pin(31)]), + continuous_split!(GPIO, [enable()] => [enable_w1ts()], 1), + // PRI_6: flash SPI mem, SPIMEM1 (SPI1) before SPIMEM0 (SPI0). The MMU + // content/index registers are intentionally excluded, + // which is why each controller's last window starts at MMU_POWER_CTRL. + continuous!(SPI1, [cmd()]..=[spi_smem_ddr()]), + continuous!(SPI1, [spi_fmem_pms_attr(0)]..=[spi_smem_ac()]), + continuous!(SPI1, [clock_gate()], 1), + continuous!(SPI1, [mmu_power_ctrl()]..=[date()]), + continuous!(SPI0, [cmd()]..=[spi_smem_ddr()]), + continuous!(SPI0, [spi_fmem_pms_attr(0)]..=[spi_smem_ac()]), + continuous!(SPI0, [clock_gate()], 1), + continuous!(SPI0, [mmu_power_ctrl()]..=[date()]), + // PRI_6: SysTimer. + systimer_seq!(SYSTIMER), ]; -// ESP32-H2 use software to trigger REGDMA to restore instead of PMU, because regdma has power bug. -pub(super) const SW_TRIGGER_REGDMA: bool = true; - -// Opt-in peripheral config-register retention data (offsets/masks/maps/counts), -// consumed by the chip-agnostic sequence builders in `retention`. -// // UART: ESP-IDF v5.4 `uart_periph.c` `UART_SLEEP_RETENTION_ENTRIES`, `uart_reg.h`. -pub(super) const UART_INT_ENA_OFF: u32 = 0x0C; // UART_INT_ENA_REG: ADDR_MAP window base -pub(super) const UART_REG_UPDATE_OFF: u32 = 0x98; // UART_REG_UPDATE_REG -pub(super) const UART_REG_UPDATE: u32 = 1 << 0; -/// Registers retained (set bits in [`UART_REGS_MAP`]). -pub(super) const UART_RETENTION_REGS_CNT: u32 = 21; -/// `uart_regs_map[4]`: config registers in the INT_ENA..ID window. -pub(super) const UART_REGS_MAP: [u32; 4] = [0x007f_ff6d, 0x0000_0010, 0, 0]; +pub(super) const UART_OPS: &[PeriphOp] = &[ + periph_continuous!(UART0, [int_ena()]), + periph_continuous!(UART0, [clkdiv()]..=[rx_filt()]), + periph_continuous!(UART0, [conf0()]..=[conf1()]), + periph_continuous!(UART0, [hwfc_conf()]..=[tout_conf()]), + periph_continuous!(UART0, [id()]), + // Restore-only: pulse REG_UPDATE to latch the shadow (`_SYNC`) registers. + periph_write!(UART0, [reg_update()], 1 << 0, 1 << 0), + periph_wait!(UART0, [reg_update()], 0, 1 << 0), +]; // I2C: ESP-IDF v5.4 `i2c_periph.c` `i2c0_regs_retention`, `i2c_reg.h`. -pub(super) const I2C_SCL_LOW_PERIOD_OFF: u32 = 0x00; // I2C_SCL_LOW_PERIOD_REG: ADDR_MAP window base -pub(super) const I2C_CTR_OFF: u32 = 0x04; // I2C_CTR_REG -pub(super) const I2C_FSM_RST: u32 = 1 << 10; // I2C_FSM_RST (value == mask) -pub(super) const I2C_CONF_UPGATE: u32 = 1 << 11; // I2C_CONF_UPGATE (value == mask) -/// Registers retained (set bits in [`I2C_REGS_MAP`]). -pub(super) const I2C_RETENTION_REGS_CNT: u32 = 18; -/// `i2c0_regs_map[4]`: config registers in the `SCL_LOW_PERIOD..SCL_STRETCH_CONF` window. -pub(super) const I2C_REGS_MAP: [u32; 4] = [0xc03f_345b, 0x3, 0, 0]; +pub(super) const I2C_OPS: &[PeriphOp] = &[ + periph_continuous!(I2C0, [scl_low_period()]..=[ctr()]), + periph_continuous!(I2C0, [to()]..=[slave_addr()]), + periph_continuous!(I2C0, [fifo_conf()]), + periph_continuous!(I2C0, [int_ena()]), + periph_continuous!(I2C0, [sda_hold()]..=[sda_sample()]), + periph_continuous!(I2C0, [scl_start_hold()]..=[clk_conf()]), + periph_continuous!(I2C0, [scl_st_time_out()]..=[scl_stretch_conf()]), + // Restore-only: pulse FSM reset, request a config update, wait for it to latch. + periph_write!(I2C0, [ctr()], 1 << 10, 1 << 10), // I2C_FSM_RST + periph_write!(I2C0, [ctr()], 0, 1 << 10), + periph_write!(I2C0, [ctr()], 1 << 11, 1 << 11), // I2C_CONF_UPGATE + periph_wait!(I2C0, [ctr()], 0, 1 << 11), +]; -// GPSPI2: ESP-IDF v5.4 `spi_periph.c` `spi2_regs_retention`, `spi_reg.h`. -pub(super) const SPI_CMD_OFF: u32 = 0x00; // SPI_CMD_REG: ADDR_MAP window base -/// Registers retained (set bits in [`SPI_REGS_MAP`]). -pub(super) const SPI_RETENTION_REGS_CNT: u32 = 12; -/// `spi_regs_map[4]`: config registers in the `CMD..SLAVE` window. -pub(super) const SPI_REGS_MAP: [u32; 4] = [0x0000_31ff, 0x0100_0000, 0, 0]; +// GPSPI2: ESP-IDF v5.4 `spi_periph.c` `spi2_regs_retention`, `spi_reg.h`. The +// config registers are only reachable while the SPI function clock runs; +// `Spi::with_retention_memory` holds that clock across the retention lifetime. +pub(super) const SPI_OPS: &[PeriphOp] = &[ + periph_continuous!(SPI2, [cmd()]..=[misc()]), + periph_continuous!(SPI2, [dma_conf()]..=[dma_int_ena()]), + periph_continuous!(SPI2, [slave()]), +]; + +// Interrupt matrix priorities. +pub(crate) const INTPRI_REGIONS: [Region; 2] = [ + region!(INTPRI, [cpu_int_enable()]..=[rnd_eco_low()]), + region!(INTPRI, [rnd_eco_high()]), +]; -// CPU-domain device-register bases lost when `pd_cpu` powers down (consumed by -// `cpu_retention`). Identical to the C6 (same RISC-V core/cache/PLIC/CLINT). -pub(crate) const INTPRI_BASE: u32 = 0x600C_5000; // interrupt priority (INTPRI) -pub(crate) const CACHE_BASE: u32 = 0x600C_8000; // L1 cache control (CACHE) -pub(crate) const PLIC_MX_BASE: u32 = 0x2000_1000; // PLIC machine interrupts -pub(crate) const PLIC_UX_BASE: u32 = 0x2000_1400; // PLIC user interrupts -pub(crate) const CLINT_MINT_BASE: u32 = 0x2000_1800; // CLINT machine timer -pub(crate) const CLINT_UINT_BASE: u32 = 0x2000_1C00; // CLINT user timer +// L1 cache control. +pub(crate) const CACHE_REGIONS: [Region; 2] = [ + region!(CACHE, [l1_cache_ctrl()]), + region!(CACHE, [l1_cache_wrap_around_ctrl()]), +]; + +// PLIC machine/user interrupt controllers. +pub(crate) const PLIC_REGIONS: [Region; 4] = [ + region!(PLIC_MX, [mxint_enable()]..=[mxint_claim()]), + region!(PLIC_MX, [mxint_conf()]), + region!(PLIC_UX, [uxint_enable()]..=[uxint_claim()]), + region!(PLIC_UX, [uxint_conf()]), +]; + +// CLINT machine/user timers. The comparators are 64-bit, which `span!` accounts +// for. +pub(crate) const CLINT_REGIONS: [Region; 2] = [ + region!(CLINT, [msip()]..=[mtimecmp()]), + region!(CLINT, [usip()]..=[utimecmp()]), +]; diff --git a/esp-hal/src/rtc_cntl/sleep/esp32c6.rs b/esp-hal/src/rtc_cntl/sleep/esp32c6.rs index a7d2d880ecc..0d9c0dd45b0 100644 --- a/esp-hal/src/rtc_cntl/sleep/esp32c6.rs +++ b/esp-hal/src/rtc_cntl/sleep/esp32c6.rs @@ -568,8 +568,8 @@ impl SleepTimeConfig { pub struct RtcSleepConfig { /// Deep Sleep flag pub deep: bool, - /// Power Down flags. On the C6 `apply()` sets the `pd_cpu`/`pd_top` bits, so - /// a domain can't power off without the caller's retention storage. + /// Power Down flags. `apply()` sets the `pd_cpu`/`pd_top` bits, so a domain + /// can't power off without the caller's retention storage. pub(crate) pd_flags: PowerDownFlags, /// Light-sleep CPU/TOP power-down retention (opt-in choices + caller memory). retention: crate::rtc_cntl::retention::SleepRetention, diff --git a/esp-hal/src/rtc_cntl/sleep/esp32h2.rs b/esp-hal/src/rtc_cntl/sleep/esp32h2.rs index 26cc691793a..5d441927704 100644 --- a/esp-hal/src/rtc_cntl/sleep/esp32h2.rs +++ b/esp-hal/src/rtc_cntl/sleep/esp32h2.rs @@ -570,16 +570,13 @@ impl RtcSleepConfig { // Light sleep: the digital domain (CPU, RAM, peripherals) stays // powered and only clock-gated by default, so execution resumes in // place. Power down the analog clock sources nothing needs while the - // core is clock-gated to cut power. Unlike the C5/C6 family, H2's - // light-sleep analog config does not lower the HP voltage, so this - // saves the oscillator current but not regulator power. + // core is clock-gated to cut power. self.pd_flags.set_pd_xtal(true); self.pd_flags.set_pd_rc_fast(true); self.pd_flags.set_pd_xtal32k(!lp_slow_uses_xtal32k); // A domain only powers down with the caller's retention storage and - // no active lock (else clock-gating); pd_top implies pd_cpu. Shares - // the chip-agnostic regDMA/CPU-retention path with the C6. + // no active lock (else clock-gating); pd_top implies pd_cpu. let (cpu_pd, top_pd) = self.retention.resolve(); self.pd_flags.set_pd_top(top_pd); self.pd_flags.set_pd_cpu(cpu_pd); diff --git a/esp-hal/src/soc/csr.rs b/esp-hal/src/soc/csr.rs new file mode 100644 index 00000000000..98b3b12b4f0 --- /dev/null +++ b/esp-hal/src/soc/csr.rs @@ -0,0 +1,183 @@ +//! Accessors for the control and status registers of CPU core. +//! +//! Covers the CSRs that light-sleep CPU retention saves and restores (see +//! [`crate::rtc_cntl::cpu_retention`]), so that the retention code can name +//! registers instead of numbers. The `riscv` crate models some of them; the +//! ones it does not know about are defined here, with the addresses ESP-IDF's +//! `csr.h` and the TRM's "RISC-V CPU" chapter use. + +/// Defines CSR accessor modules shaped like the ones in `riscv::register`: a +/// safe `read() -> usize` and an `unsafe write(usize)`, so that callers can +/// treat CSRs from either crate the same way. +/// +/// There are three forms. The bare `name = address` form defines a CSR the +/// `riscv` crate does not model, using that crate's accessor macros. The +/// `bits:` and `typed:` forms wrap CSRs it does model, but as a typed value +/// that callers dealing in raw register contents cannot use directly; `typed:` +/// is for the ones whose write side is typed as well. +macro_rules! define_csrs { + (bits: $( + $(#[$meta:meta])* + $name:ident + ),+ $(,)?) => { + $( + $(#[$meta])* + #[allow(dead_code)] + pub(crate) mod $name { + #[inline(always)] + pub(crate) fn read() -> usize { + riscv::register::$name::read().bits + } + + #[inline(always)] + pub(crate) unsafe fn write(bits: usize) { + unsafe { riscv::register::$name::write(bits) } + } + } + )+ + }; + + (typed: $( + $(#[$meta:meta])* + $name:ident as $ty:ident + ),+ $(,)?) => { + $( + $(#[$meta])* + #[allow(dead_code)] + pub(crate) mod $name { + #[inline(always)] + pub(crate) fn read() -> usize { + riscv::register::$name::read().bits() + } + + #[inline(always)] + pub(crate) unsafe fn write(bits: usize) { + let value = riscv::register::$name::$ty::from_bits(bits); + unsafe { riscv::register::$name::write(value) } + } + } + )+ + }; + + ($( + $(#[$meta:meta])* + $name:ident = $addr:literal + ),+ $(,)?) => { + $( + $(#[$meta])* + #[allow(dead_code)] + pub(crate) mod $name { + riscv::read_csr_as_usize!($addr); + riscv::write_csr_as_usize!($addr); + } + )+ + }; +} + +// CSRs whose `riscv` accessors already deal in raw bits. +pub(crate) use riscv::register::{ + mscratch, + pmpaddr0, + pmpaddr1, + pmpaddr2, + pmpaddr3, + pmpaddr4, + pmpaddr5, + pmpaddr6, + pmpaddr7, + pmpaddr8, + pmpaddr9, + pmpaddr10, + pmpaddr11, + pmpaddr12, + pmpaddr13, + pmpaddr14, + pmpaddr15, +}; + +define_csrs! { + typed: + /// Machine interrupt delegation. + mideleg as Mideleg, +} + +define_csrs! { + bits: + /// Physical memory protection configuration, entries 0..=3. + pmpcfg0, + /// Physical memory protection configuration, entries 4..=7. + pmpcfg1, + /// Physical memory protection configuration, entries 8..=11. + pmpcfg2, + /// Physical memory protection configuration, entries 12..=15. + pmpcfg3, +} + +define_csrs! { + /// ISA and supported extensions. The `riscv` crate models this read-only; + /// ESP-IDF writes it back after CPU power-down, so retention needs the + /// write side as well. + misa = 0x301, + + // Debug trigger module. + tselect = 0x7A0, + tdata1 = 0x7A1, + tdata2 = 0x7A2, + tcontrol = 0x7A5, + + // Physical memory attributes. + pmaaddr0 = 0xBD0, + pmaaddr1 = 0xBD1, + pmaaddr2 = 0xBD2, + pmaaddr3 = 0xBD3, + pmaaddr4 = 0xBD4, + pmaaddr5 = 0xBD5, + pmaaddr6 = 0xBD6, + pmaaddr7 = 0xBD7, + pmaaddr8 = 0xBD8, + pmaaddr9 = 0xBD9, + pmaaddr10 = 0xBDA, + pmaaddr11 = 0xBDB, + pmaaddr12 = 0xBDC, + pmaaddr13 = 0xBDD, + pmaaddr14 = 0xBDE, + pmaaddr15 = 0xBDF, + pmacfg0 = 0xBC0, + pmacfg1 = 0xBC1, + pmacfg2 = 0xBC2, + pmacfg3 = 0xBC3, + pmacfg4 = 0xBC4, + pmacfg5 = 0xBC5, + pmacfg6 = 0xBC6, + pmacfg7 = 0xBC7, + pmacfg8 = 0xBC8, + pmacfg9 = 0xBC9, + pmacfg10 = 0xBCA, + pmacfg11 = 0xBCB, + pmacfg12 = 0xBCC, + pmacfg13 = 0xBCD, + pmacfg14 = 0xBCE, + pmacfg15 = 0xBCF, + + // User-mode trap handling. + ustatus = 0x000, + utvec = 0x005, + uepc = 0x041, + ucause = 0x042, + + // Machine-mode performance counters. + mpcer = 0x7E0, + mpcmr = 0x7E1, + mpccr = 0x7E2, + cpu_testbus_ctrl = 0x7E3, + + // User-mode performance counters. + upcer = 0x800, + upcmr = 0x801, + upccr = 0x802, + + // Dedicated GPIO. + ugpio_oen = 0x803, + ugpio_in = 0x804, + ugpio_out = 0x805, +} diff --git a/esp-hal/src/soc/mod.rs b/esp-hal/src/soc/mod.rs index 7d7f5c1b6d1..491cfbe81e2 100644 --- a/esp-hal/src/soc/mod.rs +++ b/esp-hal/src/soc/mod.rs @@ -20,6 +20,9 @@ use crate::efuse::ChipRevision; #[cfg_attr(esp32s31, path = "esp32s31/mod.rs")] mod implementation; +#[cfg(cpu_csr_set = "esp_riscv")] +pub(crate) mod csr; + cfg_select! { all(feature = "unstable", ulp_riscv_driver_supported) => { pub use self::implementation::*; diff --git a/esp-metadata-generated/src/_build_script_utils.rs b/esp-metadata-generated/src/_build_script_utils.rs index 5080ddf9839..85612927883 100644 --- a/esp-metadata-generated/src/_build_script_utils.rs +++ b/esp-metadata-generated/src/_build_script_utils.rs @@ -393,6 +393,7 @@ impl Chip { "sleep_has_wakeup_source_ulp", "sleep_has_wakeup_source_bt", "soc_multi_core_enabled", + "soc_cpu_csr_set_is_set", "soc_cpu_mcause_mask=\"0\"", "soc_has_clock_node_xtal_clk", "soc_has_clock_node_pll_clk", @@ -633,6 +634,7 @@ impl Chip { "cargo:rustc-cfg=sleep_has_wakeup_source_ulp", "cargo:rustc-cfg=sleep_has_wakeup_source_bt", "cargo:rustc-cfg=soc_multi_core_enabled", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"0\"", "cargo:rustc-cfg=soc_has_clock_node_xtal_clk", "cargo:rustc-cfg=soc_has_clock_node_pll_clk", @@ -992,6 +994,7 @@ impl Chip { "sleep_has_wakeup_source_ulp", "sleep_has_wakeup_source_bt", "soc_cpu_has_csr_pc", + "soc_cpu_csr_set_is_set", "soc_has_swd_watchdog", "soc_cpu_mcause_mask=\"31\"", "soc_has_clock_node_xtal_clk", @@ -1178,6 +1181,7 @@ impl Chip { "cargo:rustc-cfg=sleep_has_wakeup_source_ulp", "cargo:rustc-cfg=sleep_has_wakeup_source_bt", "cargo:rustc-cfg=soc_cpu_has_csr_pc", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_has_swd_watchdog", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"31\"", "cargo:rustc-cfg=soc_has_clock_node_xtal_clk", @@ -1545,6 +1549,7 @@ impl Chip { "sleep_has_wakeup_source_ulp", "sleep_has_wakeup_source_bt", "soc_cpu_has_csr_pc", + "soc_cpu_csr_set_is_set", "soc_has_swd_watchdog", "soc_cpu_mcause_mask=\"31\"", "soc_has_clock_node_xtal_clk", @@ -1794,6 +1799,7 @@ impl Chip { "cargo:rustc-cfg=sleep_has_wakeup_source_ulp", "cargo:rustc-cfg=sleep_has_wakeup_source_bt", "cargo:rustc-cfg=soc_cpu_has_csr_pc", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_has_swd_watchdog", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"31\"", "cargo:rustc-cfg=soc_has_clock_node_xtal_clk", @@ -2201,6 +2207,7 @@ impl Chip { "soc_cpu_has_branch_predictor", "soc_cpu_csr_prv_mode=\"2064\"", "soc_cpu_csr_prv_mode_is_set", + "soc_cpu_csr_set_is_set", "soc_has_swd_watchdog", "soc_cpu_mcause_mask=\"31\"", "soc_has_clock_node_xtal_clk", @@ -2494,6 +2501,7 @@ impl Chip { "cargo:rustc-cfg=soc_cpu_has_branch_predictor", "cargo:rustc-cfg=soc_cpu_csr_prv_mode=\"2064\"", "cargo:rustc-cfg=soc_cpu_csr_prv_mode_is_set", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_has_swd_watchdog", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"31\"", "cargo:rustc-cfg=soc_has_clock_node_xtal_clk", @@ -2954,6 +2962,8 @@ impl Chip { "soc_cpu_has_csr_pc", "soc_cpu_csr_prv_mode=\"3088\"", "soc_cpu_csr_prv_mode_is_set", + "cpu_csr_set=\"esp_riscv\"", + "soc_cpu_csr_set_is_set", "soc_has_swd_watchdog", "soc_cpu_mcause_mask=\"31\"", "soc_has_clock_node_xtal_clk", @@ -3265,6 +3275,8 @@ impl Chip { "cargo:rustc-cfg=soc_cpu_has_csr_pc", "cargo:rustc-cfg=soc_cpu_csr_prv_mode=\"3088\"", "cargo:rustc-cfg=soc_cpu_csr_prv_mode_is_set", + "cargo:rustc-cfg=cpu_csr_set=\"esp_riscv\"", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_has_swd_watchdog", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"31\"", "cargo:rustc-cfg=soc_has_clock_node_xtal_clk", @@ -3665,6 +3677,7 @@ impl Chip { "soc_cpu_has_branch_predictor", "soc_cpu_csr_prv_mode=\"2064\"", "soc_cpu_csr_prv_mode_is_set", + "soc_cpu_csr_set_is_set", "soc_has_swd_watchdog", "soc_cpu_mcause_mask=\"31\"", "soc_has_clock_node_xtal_clk", @@ -3899,6 +3912,7 @@ impl Chip { "cargo:rustc-cfg=soc_cpu_has_branch_predictor", "cargo:rustc-cfg=soc_cpu_csr_prv_mode=\"2064\"", "cargo:rustc-cfg=soc_cpu_csr_prv_mode_is_set", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_has_swd_watchdog", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"31\"", "cargo:rustc-cfg=soc_has_clock_node_xtal_clk", @@ -4334,9 +4348,13 @@ impl Chip { "sleep_has_wakeup_source_bt", "sleep_has_wakeup_source_lp_core", "sleep_pd_retention", + "sleep_regdma_sw_trigger", + "sleep_regdma_wait_ops", "soc_cpu_has_csr_pc", "soc_cpu_csr_prv_mode=\"3088\"", "soc_cpu_csr_prv_mode_is_set", + "cpu_csr_set=\"esp_riscv\"", + "soc_cpu_csr_set_is_set", "soc_has_swd_watchdog", "soc_cpu_mcause_mask=\"31\"", "soc_has_clock_node_xtal_clk", @@ -4614,9 +4632,13 @@ impl Chip { "cargo:rustc-cfg=sleep_has_wakeup_source_bt", "cargo:rustc-cfg=sleep_has_wakeup_source_lp_core", "cargo:rustc-cfg=sleep_pd_retention", + "cargo:rustc-cfg=sleep_regdma_sw_trigger", + "cargo:rustc-cfg=sleep_regdma_wait_ops", "cargo:rustc-cfg=soc_cpu_has_csr_pc", "cargo:rustc-cfg=soc_cpu_csr_prv_mode=\"3088\"", "cargo:rustc-cfg=soc_cpu_csr_prv_mode_is_set", + "cargo:rustc-cfg=cpu_csr_set=\"esp_riscv\"", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_has_swd_watchdog", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"31\"", "cargo:rustc-cfg=soc_has_clock_node_xtal_clk", @@ -5001,6 +5023,7 @@ impl Chip { "sleep_has_wakeup_source_timer", "soc_cpu_has_branch_predictor", "soc_multi_core_enabled", + "soc_cpu_csr_set_is_set", "soc_internal_memory_cached", "soc_has_swd_watchdog", "soc_cpu_mcause_mask=\"63\"", @@ -5272,6 +5295,7 @@ impl Chip { "cargo:rustc-cfg=sleep_has_wakeup_source_timer", "cargo:rustc-cfg=soc_cpu_has_branch_predictor", "cargo:rustc-cfg=soc_multi_core_enabled", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_internal_memory_cached", "cargo:rustc-cfg=soc_has_swd_watchdog", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"63\"", @@ -5778,6 +5802,7 @@ impl Chip { "sleep_has_wakeup_source_touch", "sleep_has_wakeup_source_ulp_riscv", "sleep_has_wakeup_source_ulp_riscv_trap", + "soc_cpu_csr_set_is_set", "soc_has_swd_watchdog", "soc_cpu_mcause_mask=\"0\"", "soc_has_clock_node_xtal_clk", @@ -6028,6 +6053,7 @@ impl Chip { "cargo:rustc-cfg=sleep_has_wakeup_source_touch", "cargo:rustc-cfg=sleep_has_wakeup_source_ulp_riscv", "cargo:rustc-cfg=sleep_has_wakeup_source_ulp_riscv_trap", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_has_swd_watchdog", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"0\"", "cargo:rustc-cfg=soc_has_clock_node_xtal_clk", @@ -6532,6 +6558,7 @@ impl Chip { "sleep_has_wakeup_source_ulp_riscv", "sleep_has_wakeup_source_ulp_riscv_trap", "soc_multi_core_enabled", + "soc_cpu_csr_set_is_set", "soc_has_swd_watchdog", "soc_cpu_mcause_mask=\"0\"", "soc_has_clock_node_xtal_clk", @@ -6834,6 +6861,7 @@ impl Chip { "cargo:rustc-cfg=sleep_has_wakeup_source_ulp_riscv", "cargo:rustc-cfg=sleep_has_wakeup_source_ulp_riscv_trap", "cargo:rustc-cfg=soc_multi_core_enabled", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_has_swd_watchdog", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"0\"", "cargo:rustc-cfg=soc_has_clock_node_xtal_clk", @@ -7257,6 +7285,7 @@ impl Chip { "soc_multi_core_enabled", "soc_cpu_csr_prv_mode=\"2064\"", "soc_cpu_csr_prv_mode_is_set", + "soc_cpu_csr_set_is_set", "soc_has_swd_watchdog", "soc_cpu_mcause_mask=\"63\"", "soc_has_clock_node_xtal_clk", @@ -7459,6 +7488,7 @@ impl Chip { "cargo:rustc-cfg=soc_multi_core_enabled", "cargo:rustc-cfg=soc_cpu_csr_prv_mode=\"2064\"", "cargo:rustc-cfg=soc_cpu_csr_prv_mode_is_set", + "cargo:rustc-cfg=soc_cpu_csr_set_is_set", "cargo:rustc-cfg=soc_has_swd_watchdog", "cargo:rustc-cfg=soc_cpu_mcause_mask=\"63\"", "cargo:rustc-cfg=soc_has_clock_node_xtal_clk", @@ -7944,6 +7974,7 @@ impl Chip { "sleep_has_wakeup_source_ulp", "sleep_has_wakeup_source_bt", "soc_multi_core_enabled", + "soc_cpu_csr_set_is_set", "soc_has_clock_node_xtal_clk", "soc_has_clock_node_pll_clk", "soc_has_clock_node_apll_clk", @@ -8178,6 +8209,8 @@ impl Chip { "wifi_has_wifi6", "esp32c61", "esp32h2", + "sleep_regdma_sw_trigger", + "sleep_regdma_wait_ops", "soc_has_clock_node_pll_f96m_clk", "soc_has_clock_node_pll_f64m_clk", "soc_has_clock_node_pll_f48m_clk", @@ -8330,6 +8363,7 @@ impl Chip { "lp_i2c_master_version, values(\"lp_i2c\",\"rtc_i2c\")", "lp_i2c_master_fifo_size, values(\"16\")", "lp_uart_ram_size, values(\"32\")", + "cpu_csr_set, values(\"esp_riscv\")", "mipi_dsi_dma_engine, values(\"VDMA\")", "sdmmc_delay_phase_num, values(\"8\",\"4\")", "usb_otg_fifo_depth_words, values(\"200\",\"256\")", diff --git a/esp-metadata-generated/src/_generated_esp32.rs b/esp-metadata-generated/src/_generated_esp32.rs index 480965cedfb..4568a456658 100644 --- a/esp-metadata-generated/src/_generated_esp32.rs +++ b/esp-metadata-generated/src/_generated_esp32.rs @@ -379,6 +379,12 @@ macro_rules! property { ("sleep.pd_retention") => { false }; + ("sleep.regdma_sw_trigger") => { + false + }; + ("sleep.regdma_wait_ops") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c2.rs b/esp-metadata-generated/src/_generated_esp32c2.rs index fdc77c21d2c..f5c84d055a3 100644 --- a/esp-metadata-generated/src/_generated_esp32c2.rs +++ b/esp-metadata-generated/src/_generated_esp32c2.rs @@ -286,6 +286,12 @@ macro_rules! property { ("sleep.pd_retention") => { false }; + ("sleep.regdma_sw_trigger") => { + false + }; + ("sleep.regdma_wait_ops") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c3.rs b/esp-metadata-generated/src/_generated_esp32c3.rs index f3ad0b2f625..8d14775ee8c 100644 --- a/esp-metadata-generated/src/_generated_esp32c3.rs +++ b/esp-metadata-generated/src/_generated_esp32c3.rs @@ -382,6 +382,12 @@ macro_rules! property { ("sleep.pd_retention") => { false }; + ("sleep.regdma_sw_trigger") => { + false + }; + ("sleep.regdma_wait_ops") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c5.rs b/esp-metadata-generated/src/_generated_esp32c5.rs index 3d31b8b883d..afaec2d1b6f 100644 --- a/esp-metadata-generated/src/_generated_esp32c5.rs +++ b/esp-metadata-generated/src/_generated_esp32c5.rs @@ -394,6 +394,12 @@ macro_rules! property { ("sleep.pd_retention") => { false }; + ("sleep.regdma_sw_trigger") => { + false + }; + ("sleep.regdma_wait_ops") => { + false + }; ("soc.cpu_has_branch_predictor") => { true }; diff --git a/esp-metadata-generated/src/_generated_esp32c6.rs b/esp-metadata-generated/src/_generated_esp32c6.rs index cdb62c3cb4f..96d92590bd8 100644 --- a/esp-metadata-generated/src/_generated_esp32c6.rs +++ b/esp-metadata-generated/src/_generated_esp32c6.rs @@ -412,6 +412,12 @@ macro_rules! property { ("sleep.pd_retention") => { true }; + ("sleep.regdma_sw_trigger") => { + false + }; + ("sleep.regdma_wait_ops") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c61.rs b/esp-metadata-generated/src/_generated_esp32c61.rs index 458314528cd..f820f72e739 100644 --- a/esp-metadata-generated/src/_generated_esp32c61.rs +++ b/esp-metadata-generated/src/_generated_esp32c61.rs @@ -328,6 +328,12 @@ macro_rules! property { ("sleep.pd_retention") => { false }; + ("sleep.regdma_sw_trigger") => { + false + }; + ("sleep.regdma_wait_ops") => { + false + }; ("soc.cpu_has_branch_predictor") => { true }; diff --git a/esp-metadata-generated/src/_generated_esp32h2.rs b/esp-metadata-generated/src/_generated_esp32h2.rs index a0e6d8e1020..e3471493f29 100644 --- a/esp-metadata-generated/src/_generated_esp32h2.rs +++ b/esp-metadata-generated/src/_generated_esp32h2.rs @@ -397,6 +397,12 @@ macro_rules! property { ("sleep.pd_retention") => { true }; + ("sleep.regdma_sw_trigger") => { + true + }; + ("sleep.regdma_wait_ops") => { + true + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32p4.rs b/esp-metadata-generated/src/_generated_esp32p4.rs index 05fa69bf839..136b8d0d08f 100644 --- a/esp-metadata-generated/src/_generated_esp32p4.rs +++ b/esp-metadata-generated/src/_generated_esp32p4.rs @@ -349,6 +349,12 @@ macro_rules! property { ("sleep.pd_retention") => { false }; + ("sleep.regdma_sw_trigger") => { + false + }; + ("sleep.regdma_wait_ops") => { + false + }; ("soc.cpu_has_branch_predictor") => { true }; diff --git a/esp-metadata-generated/src/_generated_esp32s2.rs b/esp-metadata-generated/src/_generated_esp32s2.rs index e61e68c0505..6ed21a1e3c2 100644 --- a/esp-metadata-generated/src/_generated_esp32s2.rs +++ b/esp-metadata-generated/src/_generated_esp32s2.rs @@ -373,6 +373,12 @@ macro_rules! property { ("sleep.pd_retention") => { false }; + ("sleep.regdma_sw_trigger") => { + false + }; + ("sleep.regdma_wait_ops") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32s3.rs b/esp-metadata-generated/src/_generated_esp32s3.rs index c57561f2e51..4c4520da0a0 100644 --- a/esp-metadata-generated/src/_generated_esp32s3.rs +++ b/esp-metadata-generated/src/_generated_esp32s3.rs @@ -409,6 +409,12 @@ macro_rules! property { ("sleep.pd_retention") => { false }; + ("sleep.regdma_sw_trigger") => { + false + }; + ("sleep.regdma_wait_ops") => { + false + }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata/devices/esp32c6/soc.toml b/esp-metadata/devices/esp32c6/soc.toml index b84b78a9962..fb0f030ea33 100644 --- a/esp-metadata/devices/esp32c6/soc.toml +++ b/esp-metadata/devices/esp32c6/soc.toml @@ -26,6 +26,7 @@ has_md5_bsd = true cpu_has_csr_pc = true cpu_csr_prv_mode = 0xC10 cpu_mcause_mask = 0x1f +cpu_csr_set = "esp_riscv" has_swd_watchdog = true @@ -323,8 +324,6 @@ is_lp_sys = true support_status = "partial" light_sleep = true deep_sleep = true -# esp-hal implements CPU/TOP power-down retention (software CPU retention + -# regDMA TOP retention) for light sleep on this chip. pd_retention = true wakeup_sources = { Ext1 = 1, diff --git a/esp-metadata/devices/esp32h2/soc.toml b/esp-metadata/devices/esp32h2/soc.toml index 00259e2511e..cd0a936645f 100644 --- a/esp-metadata/devices/esp32h2/soc.toml +++ b/esp-metadata/devices/esp32h2/soc.toml @@ -26,6 +26,7 @@ has_md5_bsd = true cpu_has_csr_pc = true cpu_csr_prv_mode = 0xC10 cpu_mcause_mask = 0x1f +cpu_csr_set = "esp_riscv" has_swd_watchdog = true @@ -313,9 +314,9 @@ version = 2 support_status = "partial" light_sleep = true deep_sleep = true -# esp-hal implements CPU/TOP power-down retention (software CPU retention + -# regDMA TOP retention) for light sleep on this chip. pd_retention = true +regdma_sw_trigger = true +regdma_wait_ops = true wakeup_sources = { Ext1 = 1, Gpio = 2, diff --git a/esp-metadata/src/cfg.rs b/esp-metadata/src/cfg.rs index 46f94ca3da9..591f2b811ef 100644 --- a/esp-metadata/src/cfg.rs +++ b/esp-metadata/src/cfg.rs @@ -733,10 +733,14 @@ driver_configs![ deep_sleep: bool, #[serde(default)] wakeup_sources: WakeupSources, - // esp-hal implements CPU/TOP power-down retention (software CPU retention + - // regDMA TOP retention) for light sleep on this chip. #[serde(default)] pd_retention: bool, + #[serde(default)] + regdma_sw_trigger: bool, + // Whether the chip's TOP-domain retention program includes a step that + // polls a register, as opposed to only reading and writing them. + #[serde(default)] + regdma_wait_ops: bool, } }, SocProperties { @@ -753,6 +757,8 @@ driver_configs![ #[serde(default)] cpu_csr_prv_mode: Option, #[serde(default)] + cpu_csr_set: Option, + #[serde(default)] internal_memory_cached: bool, #[serde(default)] has_swd_watchdog: bool, diff --git a/esp-metadata/src/cfg/soc.rs b/esp-metadata/src/cfg/soc.rs index ffe548a7de8..870ca970796 100644 --- a/esp-metadata/src/cfg/soc.rs +++ b/esp-metadata/src/cfg/soc.rs @@ -66,6 +66,24 @@ impl super::GenericProperty for SocConfig { } } +/// The set of CSRs a RISC-V core implements. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum CpuCsrSet { + /// The RV32IMAC core. + EspRiscv, +} + +impl super::GenericProperty for CpuCsrSet { + fn cfgs(&self) -> Option> { + let set = match self { + Self::EspRiscv => "esp_riscv", + }; + + Some(vec![format!("cpu_csr_set=\"{set}\"")]) + } +} + /// Memory region. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] diff --git a/qa-test/src/bin/sleep_timer_powerdown.rs b/qa-test/src/bin/sleep_timer_powerdown.rs index df3609c807a..be36db0dd23 100644 --- a/qa-test/src/bin/sleep_timer_powerdown.rs +++ b/qa-test/src/bin/sleep_timer_powerdown.rs @@ -20,7 +20,7 @@ //! GPIO5 is high while awake, low while asleep, to bracket each sleep on a //! current meter / logic analyzer. -//% CHIP_FILTER: esp32c6 || esp32h2 +//% CHIP_FILTER: sleep_pd_retention #![no_std] #![no_main] @@ -53,14 +53,11 @@ macro_rules! mk_static { }}; } -/// Sleep duration per round (ms), wide enough to average on a current meter. -const EVENT_MS: u64 = 1000; -/// Awake window between sleeps (ms), to separate the plateaus on a trace. +const SLEEP_MS: u64 = 1000; const AWAKE_MS: u32 = 200; -/// Rounds per mode. const ROUNDS: u32 = 3; -/// Sleep once for `EVENT_MS` and report the time slept (from the RTC) and the +/// Sleep once for `SLEEP_MS` and report the time slept (from the RTC) and the /// CPU power-down count. `marker` is driven low for the sleep window. fn sleep_round( rtc: &mut Rtc<'_>, @@ -71,7 +68,7 @@ fn sleep_round( label: &str, round: u32, ) { - let timer = TimerWakeupSource::new(Duration::from_millis(EVENT_MS)); + let timer = TimerWakeupSource::new(Duration::from_millis(SLEEP_MS)); let rtc_before = rtc.time_since_power_up().as_micros(); marker.set_low(); @@ -119,7 +116,6 @@ fn main() -> ! { // Stamp a recognizable sentinel into the TOP register so its fate is obvious // in the log; the exact value doesn't matter, only whether it survives. const SENTINEL: u32 = 0x155; - // SAFETY: driver alive, register writable and readable. let scl_configured = unsafe { (I2C0_SCL_LOW_PERIOD as *mut u32).write_volatile(SENTINEL); I2C0_SCL_LOW_PERIOD.read_volatile() @@ -128,7 +124,7 @@ fn main() -> ! { marker.set_low(); lpwr.sleep( &clock_gated, - &[&TimerWakeupSource::new(Duration::from_millis(EVENT_MS))], + &[&TimerWakeupSource::new(Duration::from_millis(SLEEP_MS))], ); marker.set_high(); // Read while the driver is still alive (TOP stayed powered, so it survives). @@ -143,7 +139,7 @@ fn main() -> ! { marker.set_low(); lpwr.sleep( &top_pd, - &[&TimerWakeupSource::new(Duration::from_millis(EVENT_MS))], + &[&TimerWakeupSource::new(Duration::from_millis(SLEEP_MS))], ); marker.set_high(); let downs_after_nc = cpu_power_down_wake_count(); @@ -155,7 +151,7 @@ fn main() -> ! { SYSTEM::regs() .i2c0_conf() .modify(|_, w| w.i2c0_clk_en().set_bit()); - // SAFETY: register readable now the clock is on; reads its reset value (0) + // register readable now the clock is on; reads its reset value (0) // because TOP lost power and reset it. let scl_after_top_pd = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; @@ -215,29 +211,24 @@ fn main() -> ! { // Retain UART1: drops the lock so its registers are saved/restored around the // power-down. Kept alive for the proof. - let _uart1 = - uart1.with_retention_memory(mk_static!(UartRetentionMemory, UartRetentionMemory::new())); - // SAFETY: driver alive, register readable. + let mut uart1_retention = UartRetentionMemory::new(); + let _uart1 = uart1.with_retention_memory(&mut uart1_retention); let clkdiv_before = unsafe { UART1_CLKDIV.read_volatile() }; // Re-acquire I2C0 (the negative control dropped it) and retain it too. - // SAFETY: the earlier I2C0 driver was dropped, so no other instance is live. let i2c0 = I2c::new( unsafe { esp_hal::peripherals::I2C0::steal() }, I2cConfig::default(), ) .unwrap(); - let _i2c0 = - i2c0.with_retention_memory(mk_static!(I2cRetentionMemory, I2cRetentionMemory::new())); - // SAFETY: driver alive, register readable. + let mut i2c0_retention = I2cRetentionMemory::new(); + let _i2c0 = i2c0.with_retention_memory(&mut i2c0_retention); let i2c_scl_before = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; - // ...and SPI2, whose CLOCK register holds the divider from `Spi::new`. const SPI2_CLOCK: *const u32 = 0x6008_100C as *const u32; let spi2 = Spi::new(peripherals.SPI2, SpiConfig::default()).unwrap(); - let _spi2 = - spi2.with_retention_memory(mk_static!(SpiRetentionMemory, SpiRetentionMemory::new())); - // SAFETY: driver alive, register readable. + let mut spi2_retention = SpiRetentionMemory::new(); + let _spi2 = spi2.with_retention_memory(&mut spi2_retention); let spi_clock_before = unsafe { SPI2_CLOCK.read_volatile() }; // Same timer wakeup, increasingly aggressive power policies. @@ -274,7 +265,6 @@ fn main() -> ! { } ); - // Same check for I2C0's timing register. let i2c_scl_after = unsafe { I2C0_SCL_LOW_PERIOD.read_volatile() }; println!( "I2C0 SCL_LOW_PERIOD: before = {:#x}, after top-powerdown = {:#x} -> {}", @@ -287,7 +277,6 @@ fn main() -> ! { } ); - // ...and for SPI2's clock register. let spi_clock_after = unsafe { SPI2_CLOCK.read_volatile() }; println!( "SPI2 CLOCK: before = {:#x}, after top-powerdown = {:#x} -> {}", From 270fbf611d7996908742f6eb967f2f3133fc0dc1 Mon Sep 17 00:00:00 2001 From: Juraj Sadel Date: Fri, 7 Aug 2026 14:47:20 +0200 Subject: [PATCH 7/8] cleanup csr, reuse riscv --- esp-hal/Cargo.toml | 2 +- esp-hal/src/rtc_cntl/cpu_retention.rs | 77 +------ esp-hal/src/soc/csr.rs | 310 ++++++++++++-------------- 3 files changed, 143 insertions(+), 246 deletions(-) diff --git a/esp-hal/Cargo.toml b/esp-hal/Cargo.toml index ed087d0d055..c5058691305 100644 --- a/esp-hal/Cargo.toml +++ b/esp-hal/Cargo.toml @@ -121,7 +121,7 @@ esp32s31 = { version = "0.1", features = ["critical-section"], optional = true, esp32p4 = { version = "0.2", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8d0a072" } [target.'cfg(target_arch = "riscv32")'.dependencies] -riscv = { version = "0.16.1" } +riscv = { version = "0.16.1", git = "https://github.com/JurajSadel/riscv.git", branch = "test" } esp-riscv-rt = { version = "0.14.0", path = "../esp-riscv-rt", optional = true } [target.'cfg(target_arch = "xtensa")'.dependencies] diff --git a/esp-hal/src/rtc_cntl/cpu_retention.rs b/esp-hal/src/rtc_cntl/cpu_retention.rs index be115439d33..3c43956f562 100644 --- a/esp-hal/src/rtc_cntl/cpu_retention.rs +++ b/esp-hal/src/rtc_cntl/cpu_retention.rs @@ -11,10 +11,8 @@ use procmacros::ram; /// [`RtcSleepConfig::with_top_power_down`](crate::rtc_cntl::sleep::RtcSleepConfig::with_top_power_down). #[instability::unstable] pub use crate::rtc_cntl::retention::SystemRetentionMemory; -use crate::{ - peripherals::{LP_AON, PMU}, - soc::csr, -}; +use crate::peripherals::{LP_AON, PMU}; +use crate::soc::csr::{NONCRITICAL_WORDS, restore_noncritical, save_noncritical}; /// Incremented only when the CPU domain actually lost and regained power. static CPU_POWERDOWN_WAKES: AtomicU32 = AtomicU32::new(0); @@ -238,77 +236,6 @@ rv_core_critical_regs_restore: frame = sym RV_CORE_CRITICAL_REGS_FRAME, ); -// --------------------------------------------------------------------------- -// Non-critical CSRs (RvCoreNonCriticalSleepFrame) -// --------------------------------------------------------------------------- - -/// Generate the slot count and save/restore routines for the non-critical CSRs -/// from one list of names. -// CSR list order matches ESP-IDF `sleep_cpu.c`. -macro_rules! noncritical_csrs { - ($($name:ident),+ $(,)?) => { - /// Non-critical CSR slot count; sizes the `noncritical` field. - const NONCRITICAL_WORDS: usize = [$(stringify!($name)),+].len(); - - #[ram] - fn save_noncritical(buf: *mut u32) { - let mut i = 0usize; - $( - unsafe { buf.add(i).write(csr::$name::read() as u32); } - i += 1; - )+ - let _ = i; - } - - #[ram] - fn restore_noncritical(buf: *const u32) { - let mut i = 0usize; - $( - unsafe { csr::$name::write(buf.add(i).read() as usize); } - i += 1; - )+ - let _ = i; - } - }; -} - -noncritical_csrs! { - mscratch, - mideleg, - misa, - tselect, - tdata1, - tdata2, - tcontrol, - pmpaddr0, pmpaddr1, pmpaddr2, pmpaddr3, - pmpaddr4, pmpaddr5, pmpaddr6, pmpaddr7, - pmpaddr8, pmpaddr9, pmpaddr10, pmpaddr11, - pmpaddr12, pmpaddr13, pmpaddr14, pmpaddr15, - pmpcfg0, pmpcfg1, pmpcfg2, pmpcfg3, - pmaaddr0, pmaaddr1, pmaaddr2, pmaaddr3, - pmaaddr4, pmaaddr5, pmaaddr6, pmaaddr7, - pmaaddr8, pmaaddr9, pmaaddr10, pmaaddr11, - pmaaddr12, pmaaddr13, pmaaddr14, pmaaddr15, - pmacfg0, pmacfg1, pmacfg2, pmacfg3, - pmacfg4, pmacfg5, pmacfg6, pmacfg7, - pmacfg8, pmacfg9, pmacfg10, pmacfg11, - pmacfg12, pmacfg13, pmacfg14, pmacfg15, - utvec, - ustatus, - uepc, - ucause, - mpcer, - mpcmr, - mpccr, - cpu_testbus_ctrl, - upcer, - upcmr, - upccr, - ugpio_oen, - ugpio_in, - ugpio_out, -} - // --------------------------------------------------------------------------- // CPU-domain device registers (INTPRI / cache / PLIC / CLINT) // --------------------------------------------------------------------------- diff --git a/esp-hal/src/soc/csr.rs b/esp-hal/src/soc/csr.rs index 98b3b12b4f0..adb399bb144 100644 --- a/esp-hal/src/soc/csr.rs +++ b/esp-hal/src/soc/csr.rs @@ -1,183 +1,153 @@ -//! Accessors for the control and status registers of CPU core. +//! CSRs saved and restored by CPU power-down retention. //! -//! Covers the CSRs that light-sleep CPU retention saves and restores (see -//! [`crate::rtc_cntl::cpu_retention`]), so that the retention code can name -//! registers instead of numbers. The `riscv` crate models some of them; the -//! ones it does not know about are defined here, with the addresses ESP-IDF's -//! `csr.h` and the TRM's "RISC-V CPU" chapter use. - -/// Defines CSR accessor modules shaped like the ones in `riscv::register`: a -/// safe `read() -> usize` and an `unsafe write(usize)`, so that callers can -/// treat CSRs from either crate the same way. -/// -/// There are three forms. The bare `name = address` form defines a CSR the -/// `riscv` crate does not model, using that crate's accessor macros. The -/// `bits:` and `typed:` forms wrap CSRs it does model, but as a typed value -/// that callers dealing in raw register contents cannot use directly; `typed:` -/// is for the ones whose write side is typed as well. -macro_rules! define_csrs { - (bits: $( - $(#[$meta:meta])* - $name:ident - ),+ $(,)?) => { - $( - $(#[$meta])* - #[allow(dead_code)] - pub(crate) mod $name { - #[inline(always)] - pub(crate) fn read() -> usize { - riscv::register::$name::read().bits - } - - #[inline(always)] - pub(crate) unsafe fn write(bits: usize) { - unsafe { riscv::register::$name::write(bits) } - } - } - )+ +//! Retention round-trips whole registers as raw bits. Entries from +//! [`riscv::register`] use `read_bits()`/`write_bits()` so typed CSRs whose +//! field masks would otherwise drop bits survive +//! the round trip. Everything else is generated here with +//! [`riscv::read_write_csr_as_usize`]. +//! +//! Order matches ESP-IDF `sleep_cpu.c`. + +use procmacros::ram; + +/// Reads one retention-list entry as raw bits. +macro_rules! retained_read { + (riscv $name:ident) => { + riscv::register::$name::read_bits() + }; + (local $name:ident) => { + $name::read() }; +} - (typed: $( - $(#[$meta:meta])* - $name:ident as $ty:ident - ),+ $(,)?) => { - $( - $(#[$meta])* - #[allow(dead_code)] - pub(crate) mod $name { - #[inline(always)] - pub(crate) fn read() -> usize { - riscv::register::$name::read().bits() - } - - #[inline(always)] - pub(crate) unsafe fn write(bits: usize) { - let value = riscv::register::$name::$ty::from_bits(bits); - unsafe { riscv::register::$name::write(value) } - } - } - )+ +/// Writes raw bits back to one retention-list entry. +macro_rules! retained_write { + (riscv $name:ident, $bits:expr) => { + unsafe { riscv::register::$name::write_bits($bits) } }; + (local $name:ident, $bits:expr) => { + unsafe { $name::write($bits) } + }; +} - ($( - $(#[$meta:meta])* - $name:ident = $addr:literal - ),+ $(,)?) => { - $( - $(#[$meta])* +/// Defines the non-critical retention list. +macro_rules! noncritical_csrs { + ($( $kind:tt $name:ident $(= $addr:literal)? ),+ $(,)?) => { + $($( #[allow(dead_code)] - pub(crate) mod $name { - riscv::read_csr_as_usize!($addr); - riscv::write_csr_as_usize!($addr); + mod $name { + riscv::read_write_csr_as_usize!($addr); } - )+ + )?)+ + + /// Non-critical CSR slot count; sizes the `noncritical` field. + pub(crate) const NONCRITICAL_WORDS: usize = [$(stringify!($name)),+].len(); + + #[ram] + pub(crate) fn save_noncritical(buf: *mut u32) { + let mut i = 0usize; + $( + let bits = retained_read!($kind $name) as u32; + unsafe { buf.add(i).write(bits) }; + i += 1; + )+ + let _ = i; + } + + #[ram] + pub(crate) fn restore_noncritical(buf: *const u32) { + let mut i = 0usize; + $( + let bits = unsafe { buf.add(i).read() } as usize; + retained_write!($kind $name, bits); + i += 1; + )+ + let _ = i; + } }; } -// CSRs whose `riscv` accessors already deal in raw bits. -pub(crate) use riscv::register::{ - mscratch, - pmpaddr0, - pmpaddr1, - pmpaddr2, - pmpaddr3, - pmpaddr4, - pmpaddr5, - pmpaddr6, - pmpaddr7, - pmpaddr8, - pmpaddr9, - pmpaddr10, - pmpaddr11, - pmpaddr12, - pmpaddr13, - pmpaddr14, - pmpaddr15, -}; - -define_csrs! { - typed: - /// Machine interrupt delegation. - mideleg as Mideleg, -} +noncritical_csrs! { + riscv mscratch, + riscv mideleg, + riscv misa, -define_csrs! { - bits: - /// Physical memory protection configuration, entries 0..=3. - pmpcfg0, - /// Physical memory protection configuration, entries 4..=7. - pmpcfg1, - /// Physical memory protection configuration, entries 8..=11. - pmpcfg2, - /// Physical memory protection configuration, entries 12..=15. - pmpcfg3, -} + riscv tselect, + riscv tdata1, + riscv tdata2, + riscv tcontrol, + + riscv pmpaddr0, + riscv pmpaddr1, + riscv pmpaddr2, + riscv pmpaddr3, + riscv pmpaddr4, + riscv pmpaddr5, + riscv pmpaddr6, + riscv pmpaddr7, + riscv pmpaddr8, + riscv pmpaddr9, + riscv pmpaddr10, + riscv pmpaddr11, + riscv pmpaddr12, + riscv pmpaddr13, + riscv pmpaddr14, + riscv pmpaddr15, + + riscv pmpcfg0, + riscv pmpcfg1, + riscv pmpcfg2, + riscv pmpcfg3, + + local pmaaddr0 = 0xbd0, + local pmaaddr1 = 0xbd1, + local pmaaddr2 = 0xbd2, + local pmaaddr3 = 0xbd3, + local pmaaddr4 = 0xbd4, + local pmaaddr5 = 0xbd5, + local pmaaddr6 = 0xbd6, + local pmaaddr7 = 0xbd7, + local pmaaddr8 = 0xbd8, + local pmaaddr9 = 0xbd9, + local pmaaddr10 = 0xbda, + local pmaaddr11 = 0xbdb, + local pmaaddr12 = 0xbdc, + local pmaaddr13 = 0xbdd, + local pmaaddr14 = 0xbde, + local pmaaddr15 = 0xbdf, + + local pmacfg0 = 0xbc0, + local pmacfg1 = 0xbc1, + local pmacfg2 = 0xbc2, + local pmacfg3 = 0xbc3, + local pmacfg4 = 0xbc4, + local pmacfg5 = 0xbc5, + local pmacfg6 = 0xbc6, + local pmacfg7 = 0xbc7, + local pmacfg8 = 0xbc8, + local pmacfg9 = 0xbc9, + local pmacfg10 = 0xbca, + local pmacfg11 = 0xbcb, + local pmacfg12 = 0xbcc, + local pmacfg13 = 0xbcd, + local pmacfg14 = 0xbce, + local pmacfg15 = 0xbcf, + + local utvec = 0x005, + local ustatus = 0x000, + local uepc = 0x041, + local ucause = 0x042, + + local mpcer = 0x7e0, + local mpcmr = 0x7e1, + local mpccr = 0x7e2, + local cpu_testbus_ctrl = 0x7e3, + + local upcer = 0x800, + local upcmr = 0x801, + local upccr = 0x802, -define_csrs! { - /// ISA and supported extensions. The `riscv` crate models this read-only; - /// ESP-IDF writes it back after CPU power-down, so retention needs the - /// write side as well. - misa = 0x301, - - // Debug trigger module. - tselect = 0x7A0, - tdata1 = 0x7A1, - tdata2 = 0x7A2, - tcontrol = 0x7A5, - - // Physical memory attributes. - pmaaddr0 = 0xBD0, - pmaaddr1 = 0xBD1, - pmaaddr2 = 0xBD2, - pmaaddr3 = 0xBD3, - pmaaddr4 = 0xBD4, - pmaaddr5 = 0xBD5, - pmaaddr6 = 0xBD6, - pmaaddr7 = 0xBD7, - pmaaddr8 = 0xBD8, - pmaaddr9 = 0xBD9, - pmaaddr10 = 0xBDA, - pmaaddr11 = 0xBDB, - pmaaddr12 = 0xBDC, - pmaaddr13 = 0xBDD, - pmaaddr14 = 0xBDE, - pmaaddr15 = 0xBDF, - pmacfg0 = 0xBC0, - pmacfg1 = 0xBC1, - pmacfg2 = 0xBC2, - pmacfg3 = 0xBC3, - pmacfg4 = 0xBC4, - pmacfg5 = 0xBC5, - pmacfg6 = 0xBC6, - pmacfg7 = 0xBC7, - pmacfg8 = 0xBC8, - pmacfg9 = 0xBC9, - pmacfg10 = 0xBCA, - pmacfg11 = 0xBCB, - pmacfg12 = 0xBCC, - pmacfg13 = 0xBCD, - pmacfg14 = 0xBCE, - pmacfg15 = 0xBCF, - - // User-mode trap handling. - ustatus = 0x000, - utvec = 0x005, - uepc = 0x041, - ucause = 0x042, - - // Machine-mode performance counters. - mpcer = 0x7E0, - mpcmr = 0x7E1, - mpccr = 0x7E2, - cpu_testbus_ctrl = 0x7E3, - - // User-mode performance counters. - upcer = 0x800, - upcmr = 0x801, - upccr = 0x802, - - // Dedicated GPIO. - ugpio_oen = 0x803, - ugpio_in = 0x804, - ugpio_out = 0x805, + local ugpio_oen = 0x803, + local ugpio_in = 0x804, + local ugpio_out = 0x805, } From 3cb265da86f281f137fa0c6f15631efad0795b0c Mon Sep 17 00:00:00 2001 From: Juraj Sadel Date: Fri, 7 Aug 2026 14:57:39 +0200 Subject: [PATCH 8/8] reviews --- esp-hal/src/rtc_cntl/retention.rs | 18 +++++++++--------- .../src/_build_script_utils.rs | 3 --- esp-metadata-generated/src/_generated_esp32.rs | 3 --- .../src/_generated_esp32c2.rs | 3 --- .../src/_generated_esp32c3.rs | 3 --- .../src/_generated_esp32c5.rs | 3 --- .../src/_generated_esp32c6.rs | 3 --- .../src/_generated_esp32c61.rs | 3 --- .../src/_generated_esp32h2.rs | 3 --- .../src/_generated_esp32p4.rs | 3 --- .../src/_generated_esp32s2.rs | 3 --- .../src/_generated_esp32s3.rs | 3 --- esp-metadata/devices/esp32h2/soc.toml | 1 - esp-metadata/src/cfg.rs | 5 +---- 14 files changed, 10 insertions(+), 47 deletions(-) diff --git a/esp-hal/src/rtc_cntl/retention.rs b/esp-hal/src/rtc_cntl/retention.rs index 5f61880868d..38ffd1175ae 100644 --- a/esp-hal/src/rtc_cntl/retention.rs +++ b/esp-hal/src/rtc_cntl/retention.rs @@ -128,7 +128,7 @@ macro_rules! write_reg { } /// A restore-only `Wait` until `(reg & mask) == value`. -#[cfg(sleep_regdma_wait_ops)] +#[allow(unused_macros)] macro_rules! wait_reg { ($peri:ident, [$($path:tt)+], $value:expr, $mask:expr) => { SysOp::Wait { @@ -683,7 +683,9 @@ enum SysOp { mask: u32, }, /// Restore-only poll of `addr` until `(reg & mask) == value`. - #[cfg(sleep_regdma_wait_ops)] + /// + /// Constructed by [`wait_reg!`]; unused on chips whose SYS `OPS` omit it. + #[allow(dead_code)] Wait { addr: fn() -> u32, value: u32, @@ -698,9 +700,10 @@ enum SysOp { /// PAU nodes emitted for one [`SysOp`]. const fn op_nodes(op: &SysOp) -> usize { match op { - SysOp::Continuous { .. } | SysOp::ContinuousSplit { .. } | SysOp::Write { .. } => 1, - #[cfg(sleep_regdma_wait_ops)] - SysOp::Wait { .. } => 1, + SysOp::Continuous { .. } + | SysOp::ContinuousSplit { .. } + | SysOp::Write { .. } + | SysOp::Wait { .. } => 1, SysOp::Uart { .. } => UART_NODE_COUNT, SysOp::Systimer { .. } => SYSTIMER_NODE_COUNT, } @@ -712,9 +715,7 @@ const fn op_words(op: &SysOp) -> usize { SysOp::Continuous { count, .. } | SysOp::ContinuousSplit { count, .. } => *count as usize, SysOp::Uart { .. } => UART_WORDS, SysOp::Systimer { .. } => SYSTIMER_CONT_WORDS, - SysOp::Write { .. } => 0, - #[cfg(sleep_regdma_wait_ops)] - SysOp::Wait { .. } => 0, + SysOp::Write { .. } | SysOp::Wait { .. } => 0, } } @@ -1161,7 +1162,6 @@ mod sys_periph { nodes[node] = RegdmaLink::write(addr(), value, mask, true, false); node += 1; } - #[cfg(sleep_regdma_wait_ops)] SysOp::Wait { addr, value, mask } => { nodes[node] = RegdmaLink::wait(addr(), value, mask, true, false); node += 1; diff --git a/esp-metadata-generated/src/_build_script_utils.rs b/esp-metadata-generated/src/_build_script_utils.rs index 85612927883..0e84f42fa8a 100644 --- a/esp-metadata-generated/src/_build_script_utils.rs +++ b/esp-metadata-generated/src/_build_script_utils.rs @@ -4349,7 +4349,6 @@ impl Chip { "sleep_has_wakeup_source_lp_core", "sleep_pd_retention", "sleep_regdma_sw_trigger", - "sleep_regdma_wait_ops", "soc_cpu_has_csr_pc", "soc_cpu_csr_prv_mode=\"3088\"", "soc_cpu_csr_prv_mode_is_set", @@ -4633,7 +4632,6 @@ impl Chip { "cargo:rustc-cfg=sleep_has_wakeup_source_lp_core", "cargo:rustc-cfg=sleep_pd_retention", "cargo:rustc-cfg=sleep_regdma_sw_trigger", - "cargo:rustc-cfg=sleep_regdma_wait_ops", "cargo:rustc-cfg=soc_cpu_has_csr_pc", "cargo:rustc-cfg=soc_cpu_csr_prv_mode=\"3088\"", "cargo:rustc-cfg=soc_cpu_csr_prv_mode_is_set", @@ -8210,7 +8208,6 @@ impl Chip { "esp32c61", "esp32h2", "sleep_regdma_sw_trigger", - "sleep_regdma_wait_ops", "soc_has_clock_node_pll_f96m_clk", "soc_has_clock_node_pll_f64m_clk", "soc_has_clock_node_pll_f48m_clk", diff --git a/esp-metadata-generated/src/_generated_esp32.rs b/esp-metadata-generated/src/_generated_esp32.rs index 4568a456658..97b0069a032 100644 --- a/esp-metadata-generated/src/_generated_esp32.rs +++ b/esp-metadata-generated/src/_generated_esp32.rs @@ -382,9 +382,6 @@ macro_rules! property { ("sleep.regdma_sw_trigger") => { false }; - ("sleep.regdma_wait_ops") => { - false - }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c2.rs b/esp-metadata-generated/src/_generated_esp32c2.rs index f5c84d055a3..99c6364c158 100644 --- a/esp-metadata-generated/src/_generated_esp32c2.rs +++ b/esp-metadata-generated/src/_generated_esp32c2.rs @@ -289,9 +289,6 @@ macro_rules! property { ("sleep.regdma_sw_trigger") => { false }; - ("sleep.regdma_wait_ops") => { - false - }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c3.rs b/esp-metadata-generated/src/_generated_esp32c3.rs index 8d14775ee8c..4b7c53cdc56 100644 --- a/esp-metadata-generated/src/_generated_esp32c3.rs +++ b/esp-metadata-generated/src/_generated_esp32c3.rs @@ -385,9 +385,6 @@ macro_rules! property { ("sleep.regdma_sw_trigger") => { false }; - ("sleep.regdma_wait_ops") => { - false - }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c5.rs b/esp-metadata-generated/src/_generated_esp32c5.rs index afaec2d1b6f..0d197ec4b37 100644 --- a/esp-metadata-generated/src/_generated_esp32c5.rs +++ b/esp-metadata-generated/src/_generated_esp32c5.rs @@ -397,9 +397,6 @@ macro_rules! property { ("sleep.regdma_sw_trigger") => { false }; - ("sleep.regdma_wait_ops") => { - false - }; ("soc.cpu_has_branch_predictor") => { true }; diff --git a/esp-metadata-generated/src/_generated_esp32c6.rs b/esp-metadata-generated/src/_generated_esp32c6.rs index 96d92590bd8..a1ae022e26e 100644 --- a/esp-metadata-generated/src/_generated_esp32c6.rs +++ b/esp-metadata-generated/src/_generated_esp32c6.rs @@ -415,9 +415,6 @@ macro_rules! property { ("sleep.regdma_sw_trigger") => { false }; - ("sleep.regdma_wait_ops") => { - false - }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32c61.rs b/esp-metadata-generated/src/_generated_esp32c61.rs index f820f72e739..f972a95a761 100644 --- a/esp-metadata-generated/src/_generated_esp32c61.rs +++ b/esp-metadata-generated/src/_generated_esp32c61.rs @@ -331,9 +331,6 @@ macro_rules! property { ("sleep.regdma_sw_trigger") => { false }; - ("sleep.regdma_wait_ops") => { - false - }; ("soc.cpu_has_branch_predictor") => { true }; diff --git a/esp-metadata-generated/src/_generated_esp32h2.rs b/esp-metadata-generated/src/_generated_esp32h2.rs index e3471493f29..38aa327f548 100644 --- a/esp-metadata-generated/src/_generated_esp32h2.rs +++ b/esp-metadata-generated/src/_generated_esp32h2.rs @@ -400,9 +400,6 @@ macro_rules! property { ("sleep.regdma_sw_trigger") => { true }; - ("sleep.regdma_wait_ops") => { - true - }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32p4.rs b/esp-metadata-generated/src/_generated_esp32p4.rs index 136b8d0d08f..4b37ac58d46 100644 --- a/esp-metadata-generated/src/_generated_esp32p4.rs +++ b/esp-metadata-generated/src/_generated_esp32p4.rs @@ -352,9 +352,6 @@ macro_rules! property { ("sleep.regdma_sw_trigger") => { false }; - ("sleep.regdma_wait_ops") => { - false - }; ("soc.cpu_has_branch_predictor") => { true }; diff --git a/esp-metadata-generated/src/_generated_esp32s2.rs b/esp-metadata-generated/src/_generated_esp32s2.rs index 6ed21a1e3c2..da327dfb17a 100644 --- a/esp-metadata-generated/src/_generated_esp32s2.rs +++ b/esp-metadata-generated/src/_generated_esp32s2.rs @@ -376,9 +376,6 @@ macro_rules! property { ("sleep.regdma_sw_trigger") => { false }; - ("sleep.regdma_wait_ops") => { - false - }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata-generated/src/_generated_esp32s3.rs b/esp-metadata-generated/src/_generated_esp32s3.rs index 4c4520da0a0..efce6e4c01a 100644 --- a/esp-metadata-generated/src/_generated_esp32s3.rs +++ b/esp-metadata-generated/src/_generated_esp32s3.rs @@ -412,9 +412,6 @@ macro_rules! property { ("sleep.regdma_sw_trigger") => { false }; - ("sleep.regdma_wait_ops") => { - false - }; ("soc.cpu_has_branch_predictor") => { false }; diff --git a/esp-metadata/devices/esp32h2/soc.toml b/esp-metadata/devices/esp32h2/soc.toml index cd0a936645f..fde8ebd0615 100644 --- a/esp-metadata/devices/esp32h2/soc.toml +++ b/esp-metadata/devices/esp32h2/soc.toml @@ -316,7 +316,6 @@ light_sleep = true deep_sleep = true pd_retention = true regdma_sw_trigger = true -regdma_wait_ops = true wakeup_sources = { Ext1 = 1, Gpio = 2, diff --git a/esp-metadata/src/cfg.rs b/esp-metadata/src/cfg.rs index 591f2b811ef..fa1ea459e86 100644 --- a/esp-metadata/src/cfg.rs +++ b/esp-metadata/src/cfg.rs @@ -735,12 +735,9 @@ driver_configs![ wakeup_sources: WakeupSources, #[serde(default)] pd_retention: bool, + // Needed to work around an H2-specific quirk. #[serde(default)] regdma_sw_trigger: bool, - // Whether the chip's TOP-domain retention program includes a step that - // polls a register, as opposed to only reading and writing them. - #[serde(default)] - regdma_wait_ops: bool, } }, SocProperties {