From 0aa75e183b84b57a05260ed0c77b57677a81c114 Mon Sep 17 00:00:00 2001 From: wuzhenghui Date: Tue, 14 Jul 2026 11:46:23 +0800 Subject: [PATCH] feat(esp_pm): add tickless idle support in WAITI mode to reduce power consumption --- components/esp_hw_support/sleep_modes.c | 7 + components/esp_pm/CMakeLists.txt | 2 +- components/esp_pm/Kconfig | 14 + .../esp_pm/include/esp_private/pm_impl.h | 19 +- components/esp_pm/linker.lf | 3 + components/esp_pm/pm_impl.c | 324 +++++++++++++++++- components/esp_system/freertos_hooks.c | 10 +- components/freertos/Kconfig | 4 + .../esp_private/freertos_idf_additions_priv.h | 18 + components/freertos/port_systick.c | 19 +- .../soc/esp32p4/include/soc/interrupts.h | 4 +- components/soc/esp32p4/interrupts.c | 4 +- 12 files changed, 407 insertions(+), 21 deletions(-) diff --git a/components/esp_hw_support/sleep_modes.c b/components/esp_hw_support/sleep_modes.c index ad2bb1307b8..fdc60e7347d 100644 --- a/components/esp_hw_support/sleep_modes.c +++ b/components/esp_hw_support/sleep_modes.c @@ -1170,9 +1170,12 @@ static esp_err_t SLEEP_FN_ATTR esp_sleep_start(uint32_t sleep_flags, esp_sleep_m #endif // Configure timer wakeup + bool timer_wakeup_armed = false; if (!should_skip_sleep && (s_config.wakeup_triggers & RTC_TIMER_TRIG_EN)) { if (timer_wakeup_prepare(sleep_duration) != ESP_OK) { should_skip_sleep = allow_sleep_rejection ? true : false; + } else { + timer_wakeup_armed = true; } } @@ -1185,6 +1188,10 @@ static esp_err_t SLEEP_FN_ATTR esp_sleep_start(uint32_t sleep_flags, esp_sleep_m } else { result = esp_sleep_start_safe(sleep_flags, reject_triggers, deep_sleep, &config); } + if (timer_wakeup_armed) { + /* Disarm leftover comparator after non-timer wakeups so it cannot fire into other LP/RTC timer users. */ + rtc_timer_hal_clear_wakeup_time(0); + } #if CONFIG_ESP_SLEEP_CACHE_SAFE_ASSERTION if (sleep_flags & RTC_SLEEP_PD_VDDSDIO) { /* Cache Suspend 2: If previous sleep powerdowned the flash, suspend cache here so that the diff --git a/components/esp_pm/CMakeLists.txt b/components/esp_pm/CMakeLists.txt index bb2274d93f8..92de9edb691 100644 --- a/components/esp_pm/CMakeLists.txt +++ b/components/esp_pm/CMakeLists.txt @@ -1,5 +1,5 @@ idf_build_get_property(target IDF_TARGET) -set(priv_requires esp_system esp_driver_gpio esp_timer esp_hal_uart) +set(priv_requires esp_system esp_driver_gpio esp_timer esp_hal_uart esp_hal_rtc_timer esp_hal_systimer hal freertos) if(${target} STREQUAL "linux") return() # This component is not supported by the POSIX/Linux simulator diff --git a/components/esp_pm/Kconfig b/components/esp_pm/Kconfig index 96a484a6b60..2a00d48333e 100644 --- a/components/esp_pm/Kconfig +++ b/components/esp_pm/Kconfig @@ -78,6 +78,20 @@ menu "Power Management" This feature is intended to be used when lower power consumption is needed while there is enough place in IRAM to place source code. + config PM_TICKLESS_IDLE_WAITI + bool "Enable tickless idle without entering light sleep" + depends on PM_ENABLE + depends on FREERTOS_USE_TICKLESS_IDLE + depends on FREERTOS_SYSTICK_USES_SYSTIMER + depends on SOC_RTC_TIMER_SUPPORTED + default n + help + When enabled, tickless idle also applies when the system does not enter + light sleep. This includes DFS-only configurations (light sleep disabled), + and cases where light sleep is enabled but not taken (for example when + blocked by ESP_PM_NO_LIGHT_SLEEP or other PM locks). This can reduce idle + power consumption in those situations. + config PM_SLP_DISABLE_GPIO bool "Disable all GPIO when chip at sleep" depends on FREERTOS_USE_TICKLESS_IDLE diff --git a/components/esp_pm/include/esp_private/pm_impl.h b/components/esp_pm/include/esp_private/pm_impl.h index 9ef9abcee78..f215672ce87 100644 --- a/components/esp_pm/include/esp_private/pm_impl.h +++ b/components/esp_pm/include/esp_private/pm_impl.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2016-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2016-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -84,8 +84,10 @@ void esp_pm_impl_switch_mode(pm_mode_t mode, pm_mode_switch_t lock_or_unlock, pm void esp_pm_impl_init(void); /** - * @brief Hook function for the idle task - * Must be called from the IDLE task on each CPU before entering waiti state. + * @brief Release the per-core RTOS PM lock so DFS can drop frequency while idle. + * + * Safe to call more than once per idle entry: subsequent calls are no-ops until + * an ISR / leave_idle() re-acquires the lock. */ void esp_pm_impl_idle_hook(void); @@ -110,6 +112,17 @@ void esp_pm_impl_dump_stats(FILE* out); */ void esp_pm_impl_waiti(void); +#if CONFIG_PM_TICKLESS_IDLE_WAITI +/** + * @brief Execute a planned tickless WAITI from the idle task, if any. + * + * Handles RTOS PM lock release around WFI. Call once from esp_vApplicationIdleHook(); + * + * @return true if tickless WAITI ran (idle hook should return early) + */ +bool esp_pm_impl_tickless_waiti(void); +#endif + /** * @brief Callback function type for peripherals to skip light sleep. * diff --git a/components/esp_pm/linker.lf b/components/esp_pm/linker.lf index ae69c7c2858..9205884578c 100644 --- a/components/esp_pm/linker.lf +++ b/components/esp_pm/linker.lf @@ -4,6 +4,9 @@ entries: if PM_RTOS_IDLE_OPT = y: pm_impl:esp_pm_impl_idle_hook (noflash) pm_impl:esp_pm_impl_waiti (noflash) + if PM_TICKLESS_IDLE_WAITI = y: + pm_impl:esp_pm_impl_tickless_waiti (noflash) + pm_impl:tickless_waiti_enter (noflash) if PM_SLP_IRAM_OPT = y: pm_impl:esp_pm_impl_get_cpu_freq (noflash) diff --git a/components/esp_pm/pm_impl.c b/components/esp_pm/pm_impl.c index a5fab99f166..587140bf5ff 100644 --- a/components/esp_pm/pm_impl.c +++ b/components/esp_pm/pm_impl.c @@ -30,7 +30,6 @@ #include "hal/uart_ll.h" #include "hal/uart_types.h" -#include "driver/gpio.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -54,6 +53,36 @@ #include "esp_memory_utils.h" #include "esp_rom_sys.h" +#if CONFIG_PM_TICKLESS_IDLE_WAITI +#include "hal/rtc_timer_hal.h" +#include "hal/systimer_ll.h" +#include "esp_private/systimer.h" +#include "esp_private/freertos_idf_additions_priv.h" +#include "esp_intr_alloc.h" +#if CONFIG_ESP_TASK_WDT_EN +#include "esp_private/esp_task_wdt.h" +#endif +#if CONFIG_ESP_INT_WDT +#include "esp_private/esp_int_wdt.h" +#endif +#if SOC_LP_PERIPH_SHARE_INTERRUPT +#include "esp_private/rtc_ctrl.h" +#endif + +#if SOC_LP_PERIPH_SHARE_INTERRUPT +#define TICKLESS_WAITI_LP_TIMER_INTR_SOURCE ETS_LP_TIMER_INTR_SOURCE +#else +#if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S31 +#define TICKLESS_WAITI_LP_TIMER_INTR_SOURCE ETS_LP_TIMER_REG_0_INTR_SOURCE +#else +#define TICKLESS_WAITI_LP_TIMER_INTR_SOURCE ETS_LP_RTC_TIMER_INTR_SOURCE +/* LP_RTC_TIMER is shared by the LP timer alarm and brownout detector (see power_supply_periph.c). */ +#define TICKLESS_WAITI_LP_TIMER_INTR_STATUS ((uint32_t)&LP_TIMER.int_st) +#define TICKLESS_WAITI_LP_TIMER_INTR_MASK BIT(31) /* LP_TIMER_SOC_WAKEUP_INT_ST */ +#endif +#endif /* !SOC_LP_PERIPH_SHARE_INTERRUPT */ +#endif /* CONFIG_PM_TICKLESS_IDLE_WAITI */ + #define MHZ (1000000) #ifdef CONFIG_FREERTOS_SYSTICK_USES_CCOUNT @@ -71,10 +100,10 @@ #define CCOMPARE_PREPARE_CYCLES_IN_FREQ_UPDATE 60 #endif // CONFIG_FREERTOS_SYSTICK_USES_CCOUNT -/* When light sleep is used, wake this number of microseconds earlier than +/* When tickless idle is used, wake this number of microseconds earlier than * the next tick. */ -#define LIGHT_SLEEP_EARLY_WAKEUP_US 100 +#define TICKLESS_IDLE_EARLY_WAKEUP_US 100 #if CONFIG_IDF_TARGET_ESP32 /* Minimal divider at which REF_CLK_FREQ can be obtained */ @@ -825,6 +854,254 @@ static inline void IRAM_ATTR other_core_should_skip_light_sleep(int core_id) #endif } +/* ---------------------------------------- Tickless WAITI Implementation ------------------------------------------ + * Use the LP/RTC timer to bound the maximum idle duration, then enter WAITI with interrupts + * enabled. The OS tick systimer keeps running during WAITI; the LP timer alarm (or any other + * interrupt) takes the CPU out of WFI. + * + * Multi-core: the LP/RTC timer has a single shared alarm comparator (timer_id=0), programmed + * with the earliest deadline among the parked cores. The LP timer interrupt is only routed to + * core 0, so its ISR calls portYIELD_CORE() on the other parked core. Whenever any core leaves + * WAITI it yields any remaining parked sibling. + * + * Two-phase park: portSUPPRESS_TICKS_AND_SLEEP() (vApplicationSleep) runs under xKernelLock with + * interrupts masked, so WFI cannot be issued there. Phase 1 arms the LP alarm, suppresses the OS + * tick, and records the wake deadline in s_waiti_plan[]; phase 2 runs in esp_vApplicationIdleHook() + * on the next idle-loop iteration to enter WFI, disarm, restore the tick, and advance xTickCount + * via xTaskCatchUpTicks() on core 0. + * esp_pm_impl_waiti() handles plain WFI for shorter idle windows. + * --------------------------------------------------------------------------------------------------------------- */ + +#if CONFIG_PM_TICKLESS_IDLE_WAITI +/* Protected by s_switch_lock */ +typedef struct { + bool valid; + int64_t deadline_us; // absolute esp_timer time at which to wake. +} tickless_waiti_plan_t; +static tickless_waiti_plan_t s_waiti_plan[CONFIG_FREERTOS_NUMBER_OF_CORES]; + +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 +static volatile uint32_t s_waiti_armed_mask; // cores that currently hold an armed LP/RTC deadline. +static volatile uint32_t s_waiti_parked_mask; // cores currently blocked in the WAITI WFI. +static uint64_t s_waiti_target_rtc[CONFIG_FREERTOS_NUMBER_OF_CORES]; // per-core wakeup deadline in RTC slow-clock ticks. +static uint64_t s_waiti_programmed_target; // deadline currently programmed into the shared comparator. +#endif + +static void IRAM_ATTR tickless_waiti_isr(void *arg) +{ + (void)arg; +#if SOC_LP_PERIPH_SHARE_INTERRUPT + SET_PERI_REG_MASK(RTC_CNTL_INT_CLR_REG, RTC_CNTL_MAIN_TIMER_INT_CLR_M); +#else + rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, 0); +#endif +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + /* The shared LP/RTC timer interrupt is only routed to core 0; wake the core1 if parked. */ + int core_id = esp_cpu_get_core_id(); + if (s_waiti_parked_mask & BIT(1 - core_id)) { + portYIELD_CORE(1 - core_id); + } +#endif +} + +/* Suppress this core's periodic OS-tick interrupt. Only the interrupt is masked; the periodic + * comparator and the systimer counter keep free-running, so the exact number of ticks elapsed while + * masked is preserved in the counter. */ +FORCE_INLINE_ATTR void tickless_waiti_suppress_sys_tick_intr(int core_id) +{ + systimer_ll_enable_alarm_int(&SYSTIMER, SYSTIMER_ALARM_OS_TICK_CORE0 + core_id, false); +} + +FORCE_INLINE_ATTR void tickless_waiti_restore_sys_tick_intr(int core_id) +{ + systimer_ll_enable_alarm_int(&SYSTIMER, SYSTIMER_ALARM_OS_TICK_CORE0 + core_id, true); +} + +FORCE_INLINE_ATTR void tickless_waiti_arm_alarm(int core_id, int64_t sleep_time_us) +{ + uint32_t cal_val = esp_clk_slowclk_cal_get(); + uint64_t rtc_ticks = rtc_time_us_to_slowclk((uint64_t)sleep_time_us, cal_val); + uint64_t now_rtc = rtc_timer_hal_get_cycle_count(0); + /* rtc_time_us_to_slowclk() truncates; a zero delta makes target == now_rtc and the + * alarm will never fire. Clamp to one slow-clock tick. */ + uint64_t target = now_rtc + MAX(rtc_ticks, 1); +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + /* The comparator is shared, so program it with the earliest deadline among all armed cores. + * Only reprogram when this core lowers the deadline or the alarm was idle, to avoid + * clobbering an earlier deadline that another core still depends on. */ + s_waiti_target_rtc[core_id] = target; + bool was_empty = (s_waiti_armed_mask == 0); + s_waiti_armed_mask |= BIT(core_id); + uint64_t min_target = target; + if ((s_waiti_armed_mask & BIT(1 - core_id)) && s_waiti_target_rtc[1 - core_id] < target) { + min_target = s_waiti_target_rtc[1 - core_id]; + } + if (was_empty || (min_target < s_waiti_programmed_target)) { + rtc_timer_hal_set_wakeup_time(0, min_target); + s_waiti_programmed_target = min_target; + } +#else + (void)core_id; + rtc_timer_hal_set_wakeup_time(0, target); +#endif +} + +FORCE_INLINE_ATTR void tickless_waiti_disarm_alarm(int core_id) +{ +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + s_waiti_armed_mask &= ~BIT(core_id); + if (s_waiti_armed_mask != 0) { + /* The leaving core may have owned the programmed min; reprogram to the earliest + * deadline still armed so the sibling is not woken early or spuriously. */ + uint64_t min_target = s_waiti_target_rtc[1 - core_id]; + if (min_target != s_waiti_programmed_target) { + rtc_timer_hal_set_wakeup_time(0, min_target); + s_waiti_programmed_target = min_target; + } + return; + } +#else + (void)core_id; +#endif + +#if SOC_LP_PERIPH_SHARE_INTERRUPT + CLEAR_PERI_REG_MASK(RTC_CNTL_SLP_TIMER1_REG, RTC_CNTL_MAIN_TIMER_ALARM_EN_M); + SET_PERI_REG_MASK(RTC_CNTL_INT_CLR_REG, RTC_CNTL_MAIN_TIMER_INT_CLR_M); +#else + rtc_timer_ll_set_target_enable(&LP_TIMER, 0, false); + rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, 0); +#endif +} + +static bool tickless_waiti_enter(void) +{ + int core_id = xPortGetCoreID(); + if (!s_skipped_light_sleep[core_id] || !s_waiti_plan[core_id].valid) { + return false; + } + + portENTER_CRITICAL(&s_switch_lock); + s_waiti_plan[core_id].valid = false; + + int64_t remaining_us = s_waiti_plan[core_id].deadline_us - esp_timer_get_time(); + if ((remaining_us < configEXPECTED_IDLE_TIME_BEFORE_SLEEP * portTICK_PERIOD_MS * 1000LL) +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + /* core 0 owns the global tick; it must not suppress it while core 1 is still active. */ + || (core_id == 0 && !(s_waiti_parked_mask & BIT(1))) +#endif + ) { + /* Abort without Claim/CatchUp: systimer counter kept running while the alarm int was + * masked, so the next SysTickIsrHandler will recover missed ticks via its diff path. */ + tickless_waiti_disarm_alarm(core_id); + tickless_waiti_restore_sys_tick_intr(core_id); + portEXIT_CRITICAL(&s_switch_lock); + return false; + } + + bool stop_global_tick = (core_id == 0); +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + s_waiti_parked_mask |= BIT(core_id); +#if CONFIG_ESP_INT_WDT && CONFIG_ESP_INT_WDT_CHECK_CPU1 + if (core_id == 1) { + /* The interrupt watchdog feed runs from core 0's tick hook and, with CPU1 liveness checking, + * requires core 1 to keep ticking. Pause that check while core 1 is intentionally idle. */ + esp_int_wdt_pause_cpu1_checking(true); + } +#endif +#endif + portEXIT_CRITICAL(&s_switch_lock); + + if (stop_global_tick) { + /* Tick hook no longer runs while the global tick is suppressed, so the watchdogs it feeds + * would time out. Pause them across the WAITI window. */ +#if CONFIG_ESP_TASK_WDT_EN + esp_task_wdt_stop(); +#endif +#if CONFIG_ESP_INT_WDT + esp_int_wdt_pause(); +#endif + } + + /* Lock-free context with interrupts enabled: the LP/RTC alarm (or any other interrupt) takes + * the CPU out of WFI, and an ISR may portYIELD_CORE() this core. */ + esp_cpu_wait_for_intr(); + + if (stop_global_tick) { +#if CONFIG_ESP_INT_WDT + esp_int_wdt_resume(); +#endif +#if CONFIG_ESP_TASK_WDT_EN + esp_task_wdt_restart(); +#endif + } + + portENTER_CRITICAL(&s_switch_lock); +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + s_waiti_parked_mask &= ~BIT(core_id); +#if CONFIG_ESP_INT_WDT && CONFIG_ESP_INT_WDT_CHECK_CPU1 + if (core_id == 1) { + esp_int_wdt_pause_cpu1_checking(false); + } +#endif + // We may have been woken by an unrelated interrupt rather than the LP timer; make sure + // the sibling leaves WAITI and restores the tick. + if (s_waiti_parked_mask & BIT(1 - core_id)) { + portYIELD_CORE(1 - core_id); + } +#endif + tickless_waiti_disarm_alarm(core_id); + uint32_t elapsed_ticks = xPortSysTickClaimElapsedTicks(core_id); + tickless_waiti_restore_sys_tick_intr(core_id); + portEXIT_CRITICAL(&s_switch_lock); + + if (core_id == 0 && elapsed_ticks > 0) { + (void) xTaskCatchUpTicks((TickType_t) elapsed_ticks); + } + + /* Wake path re-acquires the RTOS lock via leave_idle(); drop it again so + * vApplicationSleep() in this idle iteration can enter light sleep / prepare WAITI. */ + esp_pm_impl_idle_hook(); + return true; +} + +bool esp_pm_impl_tickless_waiti(void) +{ + /* Release RTOS PM lock before any WFI so DFS can drop to min_freq. */ + esp_pm_impl_idle_hook(); + return tickless_waiti_enter(); +} + +static void tickless_waiti_init(void) +{ +#if SOC_LP_PERIPH_SHARE_INTERRUPT + rtc_isr_register(tickless_waiti_isr, NULL, RTC_CNTL_MAIN_TIMER_INT_ENA_M, 0); + SET_PERI_REG_MASK(RTC_CNTL_INT_ENA_REG, RTC_CNTL_MAIN_TIMER_INT_ENA_M); +#else +#if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S31 + ESP_ERROR_CHECK(esp_intr_alloc(TICKLESS_WAITI_LP_TIMER_INTR_SOURCE, ESP_INTR_FLAG_IRAM, tickless_waiti_isr, NULL, NULL)); +#else + ESP_ERROR_CHECK(esp_intr_alloc_intrstatus(TICKLESS_WAITI_LP_TIMER_INTR_SOURCE, + ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_SHARED, + TICKLESS_WAITI_LP_TIMER_INTR_STATUS, + TICKLESS_WAITI_LP_TIMER_INTR_MASK, + tickless_waiti_isr, NULL, NULL)); +#endif + rtc_timer_ll_alarm_intr_enable(&LP_TIMER, 0, true); +#endif +} +#endif /* CONFIG_PM_TICKLESS_IDLE_WAITI */ + +// Compute how long the CPU may stay idle: the smaller of the FreeRTOS expected idle window +// and the time until the next esp_timer alarm. +FORCE_INLINE_ATTR int64_t pm_get_min_idle_us(TickType_t xExpectedIdleTime) +{ + int64_t now = esp_timer_get_time(); + int64_t next_esp_timer_alarm = esp_timer_get_next_alarm_for_wake_up(); + int64_t time_until_next_alarm = next_esp_timer_alarm - now; + int64_t wakeup_delay_us = portTICK_PERIOD_MS * 1000LL * xExpectedIdleTime; + return MIN(wakeup_delay_us, time_until_next_alarm); +} + // Adjust RTOS tick count based on the amount of time spent in sleep. FORCE_INLINE_ATTR void pm_step_tick(int64_t slept_us, TickType_t xExpectedIdleTime) { @@ -866,11 +1143,7 @@ void vApplicationSleep( TickType_t xExpectedIdleTime ) int core_id = xPortGetCoreID(); if (!should_skip_light_sleep(core_id)) { /* Calculate how much we can sleep */ - int64_t next_esp_timer_alarm = esp_timer_get_next_alarm_for_wake_up(); - int64_t now = esp_timer_get_time(); - int64_t time_until_next_alarm = next_esp_timer_alarm - now; - int64_t wakeup_delay_us = portTICK_PERIOD_MS * 1000LL * xExpectedIdleTime; - int64_t sleep_time_us = MIN(wakeup_delay_us, time_until_next_alarm); + int64_t sleep_time_us = pm_get_min_idle_us(xExpectedIdleTime); int64_t slept_us = 0; #if CONFIG_PM_LIGHT_SLEEP_CALLBACKS uint32_t cycle = esp_cpu_get_cycle_count(); @@ -878,7 +1151,7 @@ void vApplicationSleep( TickType_t xExpectedIdleTime ) sleep_time_us -= (esp_cpu_get_cycle_count() - cycle) / (esp_clk_cpu_freq() / 1000000ULL); #endif if (sleep_time_us >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP * portTICK_PERIOD_MS * 1000LL) { - esp_sleep_enable_timer_wakeup(sleep_time_us - LIGHT_SLEEP_EARLY_WAKEUP_US); + esp_sleep_enable_timer_wakeup(sleep_time_us - TICKLESS_IDLE_EARLY_WAKEUP_US); /* Enter sleep */ ESP_PM_TRACE_ENTER(SLEEP, core_id); int64_t sleep_start = esp_timer_get_time(); @@ -904,6 +1177,35 @@ void vApplicationSleep( TickType_t xExpectedIdleTime ) esp_pm_execute_exit_sleep_callbacks(slept_us); #endif } +#if CONFIG_PM_TICKLESS_IDLE_WAITI + else { + /* Light sleep was skipped. This covers both the case where light sleep is globally + * disabled (DFS-only configuration, s_light_sleep_en == false) and the case where it is + * enabled but currently forbidden (a power management lock keeps s_mode away from + * PM_MODE_LIGHT_SLEEP, a mode switch is in progress, or a peripheral vetoed sleep). In all + * of these we can still suppress the OS tick and enter WAITI to save power. + * + * WFI must run with interrupts enabled, which is not the case here (xKernelLock is held). + * esp_pm_impl_tickless_waiti() enters WFI on the next idle-loop iteration. + * On failure to tickless-park, esp_pm_impl_waiti() handles plain WFI. */ + int64_t sleep_time_us = pm_get_min_idle_us(xExpectedIdleTime); + + if ((sleep_time_us >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP * portTICK_PERIOD_MS * 1000LL) +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + /* core 0 owns the global tick; it must not suppress it while core 1 is still active. */ + && (core_id != 0 || (s_waiti_parked_mask & BIT(1))) +#endif + ) { + s_waiti_plan[core_id].deadline_us = esp_timer_get_time() + sleep_time_us; + s_waiti_plan[core_id].valid = true; + /* Wake slightly early so that slow-clock inaccuracy biases towards an early + * (rather than late) wakeup; xTaskCatchUpTicks() on core 0 recover the exact + * tick count after WFI. */ + tickless_waiti_arm_alarm(core_id, sleep_time_us - TICKLESS_IDLE_EARLY_WAKEUP_US); + tickless_waiti_suppress_sys_tick_intr(core_id); + } + } +#endif /* CONFIG_PM_TICKLESS_IDLE_WAITI */ portEXIT_CRITICAL(&s_switch_lock); } #endif //CONFIG_FREERTOS_USE_TICKLESS_IDLE @@ -1020,6 +1322,10 @@ void esp_pm_impl_init(void) s_cpu_freq_by_mode[i] = default_config; } +#if CONFIG_PM_TICKLESS_IDLE_WAITI + tickless_waiti_init(); +#endif + #ifdef CONFIG_PM_DFS_INIT_AUTO int xtal_freq_mhz = esp_clk_xtal_freq() / MHZ; esp_pm_config_t cfg = { diff --git a/components/esp_system/freertos_hooks.c b/components/esp_system/freertos_hooks.c index 49f0a49271d..e097af3b418 100644 --- a/components/esp_system/freertos_hooks.c +++ b/components/esp_system/freertos_hooks.c @@ -40,8 +40,16 @@ void esp_vApplicationTickHook(void) void esp_vApplicationIdleHook(void) { - bool can_go_idle = true; int core = xPortGetCoreID(); + +#if CONFIG_PM_TICKLESS_IDLE_WAITI + /* Two-phase WAITI: plan is armed in vApplicationSleep(); execute it here (kernel lock free). */ + if (esp_pm_impl_tickless_waiti()) { + return; + } +#endif + + bool can_go_idle = true; for (int n = 0; n < MAX_HOOKS; n++) { if (idle_cb[core][n] != NULL && !idle_cb[core][n]()) { can_go_idle = false; diff --git a/components/freertos/Kconfig b/components/freertos/Kconfig index 18acbf8e905..c5ebd4146df 100644 --- a/components/freertos/Kconfig +++ b/components/freertos/Kconfig @@ -331,6 +331,10 @@ menu "FreeRTOS" when no tasks need to run. To skip unnecessary wake-up initialize a timer with the "skip_unhandled_events" option as true. + While the OS tick interrupt is suppressed during tickless idle, tick hooks are not invoked + for those suppressed ticks. This applies to esp_register_freertos_tick_hook() callbacks and + to configUSE_TICK_HOOK. + If disabled, automatic light sleep support will be disabled. config FREERTOS_IDLE_TIME_BEFORE_SLEEP diff --git a/components/freertos/esp_additions/include/esp_private/freertos_idf_additions_priv.h b/components/freertos/esp_additions/include/esp_private/freertos_idf_additions_priv.h index 48ca003c659..87c98f91cad 100644 --- a/components/freertos/esp_additions/include/esp_private/freertos_idf_additions_priv.h +++ b/components/freertos/esp_additions/include/esp_private/freertos_idf_additions_priv.h @@ -201,6 +201,24 @@ #endif /* ( !CONFIG_FREERTOS_SMP && ( configNUM_CORES > 1 ) ) */ +#if CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER + +/* + * Reconcile the given core's OS-tick accounting against the free-running counter while its + * OS-tick interrupt is still masked. + * + * Updates s_handled_systicks using the same accounting as SysTickIsrHandler(), so + * re-enabling the alarm afterwards will not reprocess suppressed ticks. The caller should + * restore the OS-tick interrupt and then advance the RTOS tick by the returned amount. + * + * @param cpu_id Core whose suppressed OS-tick accounting should be reconciled. + * + * @return Number of whole OS-tick periods that elapsed while masked. + */ + uint32_t xPortSysTickClaimElapsedTicks( int cpu_id ); + +#endif /* CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER */ + /*------------------------------------------------------------------------------ * TASK UTILITIES (PRIVATE) *----------------------------------------------------------------------------*/ diff --git a/components/freertos/port_systick.c b/components/freertos/port_systick.c index 179d74b8390..505d41daf90 100644 --- a/components/freertos/port_systick.c +++ b/components/freertos/port_systick.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2017-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2017-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -50,6 +50,9 @@ void SysTickIsrHandler(void *arg); static uint32_t s_handled_systicks[configNUM_CORES] = { 0 }; +/* Systimer HAL layer object */ +static systimer_hal_context_t systimer_hal; + /** * @brief Set up the systimer peripheral to generate the tick interrupt * @@ -65,8 +68,6 @@ void vSystimerSetup(void) #else const unsigned level = ESP_INTR_FLAG_LEVEL1; #endif - /* Systimer HAL layer object */ - static systimer_hal_context_t systimer_hal; /* set system timer interrupt vector */ ESP_ERROR_CHECK(esp_intr_alloc(ETS_SYSTIMER_TARGET0_INTR_SOURCE + cpuid, ESP_INTR_FLAG_IRAM | level, SysTickIsrHandler, &systimer_hal, NULL)); @@ -155,6 +156,18 @@ void SysTickIsrHandler(void *arg) ESP_PM_TRACE_EXIT(TICK, cpuid); #endif } + +uint32_t xPortSysTickClaimElapsedTicks(int cpu_id) +{ + uint32_t alarm_id = SYSTIMER_ALARM_OS_TICK_CORE0 + (uint32_t)cpu_id; + uint32_t total = systimer_hal_get_counter_value(&systimer_hal, SYSTIMER_COUNTER_OS_TICK) / systimer_ll_get_alarm_period(systimer_hal.dev, alarm_id); + uint32_t elapsed = total - s_handled_systicks[cpu_id]; + if (elapsed == 0) { + return 0; + } + s_handled_systicks[cpu_id] = total; + return elapsed; +} #endif /* CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER */ /* ------------------------------------------------ Common Port Tick --------------------------------------------------- diff --git a/components/soc/esp32p4/include/soc/interrupts.h b/components/soc/esp32p4/include/soc/interrupts.h index abcc9842886..21f2bb99408 100644 --- a/components/soc/esp32p4/include/soc/interrupts.h +++ b/components/soc/esp32p4/include/soc/interrupts.h @@ -17,8 +17,8 @@ extern "C" typedef enum { ETS_LP_RTC_INTR_SOURCE = 0, ETS_LP_WDT_INTR_SOURCE, - ETS_LP_TIMER_REG0_INTR_SOURCE, - ETS_LP_TIMER_REG1_INTR_SOURCE, + ETS_LP_TIMER_REG_0_INTR_SOURCE, + ETS_LP_TIMER_REG_1_INTR_SOURCE, ETS_MB_HP_INTR_SOURCE, ETS_MB_LP_INTR_SOURCE, ETS_PMU_0_INTR_SOURCE, diff --git a/components/soc/esp32p4/interrupts.c b/components/soc/esp32p4/interrupts.c index 7e179aa7a00..598c975befa 100644 --- a/components/soc/esp32p4/interrupts.c +++ b/components/soc/esp32p4/interrupts.c @@ -9,8 +9,8 @@ const char *const esp_isr_names[] = { [ETS_LP_RTC_INTR_SOURCE] = "LP_RTC", [ETS_LP_WDT_INTR_SOURCE] = "LP_WDT", - [ETS_LP_TIMER_REG0_INTR_SOURCE] = "LP_TIMER_REG0", - [ETS_LP_TIMER_REG1_INTR_SOURCE] = "LP_TIMER_REG1", + [ETS_LP_TIMER_REG_0_INTR_SOURCE] = "LP_TIMER_REG0", + [ETS_LP_TIMER_REG_1_INTR_SOURCE] = "LP_TIMER_REG1", [ETS_MB_HP_INTR_SOURCE] = "MB_HP", [ETS_MB_LP_INTR_SOURCE] = "MB_LP", [ETS_PMU_0_INTR_SOURCE] = "PMU_0",