From 412bce6544e69b71f5345ef2306a4976b41a1df6 Mon Sep 17 00:00:00 2001 From: wuzhenghui Date: Tue, 14 Jul 2026 11:44:56 +0800 Subject: [PATCH 1/4] fix(rtc_timer): disable target before setting wakeup time to avoid intermediate states --- components/esp_hal_rtc_timer/README.md | 1 + .../esp32/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32c2/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32c3/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32c5/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32c6/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32c61/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32h2/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32h21/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32h4/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32p4/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32s2/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32s3/include/hal/rtc_timer_ll.h | 14 +++++++++++++- .../esp32s31/include/hal/rtc_timer_ll.h | 12 ++++++++++++ .../esp_hal_rtc_timer/include/hal/rtc_timer_hal.h | 12 +++++++++++- .../test_apps/main/test_rtc_timer.c | 1 - 16 files changed, 180 insertions(+), 14 deletions(-) diff --git a/components/esp_hal_rtc_timer/README.md b/components/esp_hal_rtc_timer/README.md index ae52a7baed1..57235984e1c 100644 --- a/components/esp_hal_rtc_timer/README.md +++ b/components/esp_hal_rtc_timer/README.md @@ -14,6 +14,7 @@ The component consolidates timer functionality from two different hardware imple ### Unified HAL API (All Chips) - Wakeup timer configuration via `rtc_timer_hal_set_wakeup_time()` +- Wakeup timer clear/disarm via `rtc_timer_hal_clear_wakeup_time()` - RTC time reading via `rtc_timer_hal_get_cycle_count()` ### Hardware-Specific LL API diff --git a/components/esp_hal_rtc_timer/esp32/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32/include/hal/rtc_timer_ll.h index f11394dab53..c3e58693957 100644 --- a/components/esp_hal_rtc_timer/esp32/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -30,6 +30,18 @@ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t t WRITE_PERI_REG(RTC_CNTL_SLP_TIMER1_REG, t >> 32); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID (ignored for V1 hardware, only one timer supported) + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + (void)timer_id; // V1 hardware only supports one timer + 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); +} + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/esp32c2/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32c2/include/hal/rtc_timer_ll.h index 88d03af43d3..5c2f1b76528 100644 --- a/components/esp_hal_rtc_timer/esp32c2/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32c2/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -31,6 +31,18 @@ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t t SET_PERI_REG_MASK(RTC_CNTL_SLP_TIMER1_REG, RTC_CNTL_MAIN_TIMER_ALARM_EN_M); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID (ignored for V1 hardware, only one timer supported) + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + (void)timer_id; // V1 hardware only supports one timer + 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); +} + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/esp32c3/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32c3/include/hal/rtc_timer_ll.h index 88d03af43d3..5c2f1b76528 100644 --- a/components/esp_hal_rtc_timer/esp32c3/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32c3/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -31,6 +31,18 @@ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t t SET_PERI_REG_MASK(RTC_CNTL_SLP_TIMER1_REG, RTC_CNTL_MAIN_TIMER_ALARM_EN_M); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID (ignored for V1 hardware, only one timer supported) + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + (void)timer_id; // V1 hardware only supports one timer + 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); +} + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/esp32c5/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32c5/include/hal/rtc_timer_ll.h index 69a90ebf784..bdc28591124 100644 --- a/components/esp_hal_rtc_timer/esp32c5/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32c5/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -103,11 +103,23 @@ FORCE_INLINE_ATTR void rtc_timer_ll_alarm_intr_enable(lp_timer_dev_t *dev, uint8 */ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t ticks) { + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); rtc_timer_ll_set_alarm_target(&LP_TIMER, timer_id, ticks); rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, true); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); + rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); +} + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/esp32c6/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32c6/include/hal/rtc_timer_ll.h index e8453112c78..3636e5697d4 100644 --- a/components/esp_hal_rtc_timer/esp32c6/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32c6/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -103,11 +103,23 @@ FORCE_INLINE_ATTR void rtc_timer_ll_alarm_intr_enable(lp_timer_dev_t *dev, uint8 */ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t ticks) { + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); rtc_timer_ll_set_alarm_target(&LP_TIMER, timer_id, ticks); rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, true); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); + rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); +} + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/esp32c61/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32c61/include/hal/rtc_timer_ll.h index 8ca7fd007ae..d45c641856c 100644 --- a/components/esp_hal_rtc_timer/esp32c61/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32c61/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -103,11 +103,23 @@ FORCE_INLINE_ATTR void rtc_timer_ll_alarm_intr_enable(lp_timer_dev_t *dev, uint8 */ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t ticks) { + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); rtc_timer_ll_set_alarm_target(&LP_TIMER, timer_id, ticks); rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, true); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); + rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); +} + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/esp32h2/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32h2/include/hal/rtc_timer_ll.h index cac6610bb9a..a77dfd02261 100644 --- a/components/esp_hal_rtc_timer/esp32h2/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32h2/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -92,11 +92,23 @@ FORCE_INLINE_ATTR void rtc_timer_ll_alarm_intr_enable(lp_timer_dev_t *dev, uint8 */ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t ticks) { + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); rtc_timer_ll_set_alarm_target(&LP_TIMER, timer_id, ticks); rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, true); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); + rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); +} + /** * @brief Get current RTC timer cycle count * Note: For ESP32-H2, counter[timer_id] is used after snapshot diff --git a/components/esp_hal_rtc_timer/esp32h21/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32h21/include/hal/rtc_timer_ll.h index b7e9b7c6e8d..59ce7e04042 100644 --- a/components/esp_hal_rtc_timer/esp32h21/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32h21/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -92,11 +92,23 @@ FORCE_INLINE_ATTR void rtc_timer_ll_alarm_intr_enable(lp_timer_dev_t *dev, uint8 */ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t ticks) { + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); rtc_timer_ll_set_alarm_target(&LP_TIMER, timer_id, ticks); rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, true); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); + rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); +} + /** * @brief Get current RTC timer cycle count * Note: For ESP32-H21, counter[timer_id] is used after snapshot diff --git a/components/esp_hal_rtc_timer/esp32h4/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32h4/include/hal/rtc_timer_ll.h index b891a92753e..dcca4c16c73 100644 --- a/components/esp_hal_rtc_timer/esp32h4/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32h4/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -109,11 +109,23 @@ FORCE_INLINE_ATTR void rtc_timer_ll_record_regdma_work_time_enable(lp_timer_dev_ */ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t ticks) { + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); rtc_timer_ll_set_alarm_target(&LP_TIMER, timer_id, ticks); rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, true); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); + rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); +} + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/esp32p4/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32p4/include/hal/rtc_timer_ll.h index 4651754a581..d4d66180133 100644 --- a/components/esp_hal_rtc_timer/esp32p4/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32p4/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -103,11 +103,23 @@ FORCE_INLINE_ATTR void rtc_timer_ll_alarm_intr_enable(lp_timer_dev_t *dev, uint8 */ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t ticks) { + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); rtc_timer_ll_set_alarm_target(&LP_TIMER, timer_id, ticks); rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, true); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); + rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); +} + /** * @brief Get current RTC timer cycle count * Note: For ESP32-P4, counter[timer_id] is used after snapshot diff --git a/components/esp_hal_rtc_timer/esp32s2/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32s2/include/hal/rtc_timer_ll.h index 415379f8abc..5d9206e6616 100644 --- a/components/esp_hal_rtc_timer/esp32s2/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32s2/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -31,6 +31,18 @@ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t t SET_PERI_REG_MASK(RTC_CNTL_SLP_TIMER1_REG, RTC_CNTL_MAIN_TIMER_ALARM_EN_M); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID (ignored for V1 hardware, only one timer supported) + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + (void)timer_id; // V1 hardware only supports one timer + 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); +} + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/esp32s3/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32s3/include/hal/rtc_timer_ll.h index 7e33a7f1370..ed2c12bc36d 100644 --- a/components/esp_hal_rtc_timer/esp32s3/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32s3/include/hal/rtc_timer_ll.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -32,6 +32,18 @@ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t t SET_PERI_REG_MASK(RTC_CNTL_SLP_TIMER1_REG, RTC_CNTL_MAIN_TIMER_ALARM_EN_M); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID (ignored for V1 hardware, only one timer supported) + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + (void)timer_id; // V1 hardware only supports one timer + 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); +} + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/esp32s31/include/hal/rtc_timer_ll.h b/components/esp_hal_rtc_timer/esp32s31/include/hal/rtc_timer_ll.h index db8ce18894a..3868a7c2e75 100644 --- a/components/esp_hal_rtc_timer/esp32s31/include/hal/rtc_timer_ll.h +++ b/components/esp_hal_rtc_timer/esp32s31/include/hal/rtc_timer_ll.h @@ -106,11 +106,23 @@ FORCE_INLINE_ATTR void rtc_timer_ll_alarm_intr_enable(rtc_timer_dev_t *dev, uint */ FORCE_INLINE_ATTR void rtc_timer_ll_set_wakeup_time(uint8_t timer_id, uint64_t ticks) { + rtc_timer_ll_set_target_enable(&RTC_TIMER, timer_id, false); rtc_timer_ll_clear_alarm_intr_status(&RTC_TIMER, timer_id); rtc_timer_ll_set_alarm_target(&RTC_TIMER, timer_id, ticks); rtc_timer_ll_set_target_enable(&RTC_TIMER, timer_id, true); } +/** + * @brief Clear/disarm the wakeup timer alarm enabled by rtc_timer_ll_set_wakeup_time() + * + * @param timer_id Timer ID + */ +FORCE_INLINE_ATTR void rtc_timer_ll_clear_wakeup_time(uint8_t timer_id) +{ + rtc_timer_ll_set_target_enable(&RTC_TIMER, timer_id, false); + rtc_timer_ll_clear_alarm_intr_status(&RTC_TIMER, timer_id); +} + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/include/hal/rtc_timer_hal.h b/components/esp_hal_rtc_timer/include/hal/rtc_timer_hal.h index e3bd75ee701..5cb2e7b3e1f 100644 --- a/components/esp_hal_rtc_timer/include/hal/rtc_timer_hal.h +++ b/components/esp_hal_rtc_timer/include/hal/rtc_timer_hal.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -23,6 +23,16 @@ extern "C" { */ #define rtc_timer_hal_set_wakeup_time(timer_id, ticks) rtc_timer_ll_set_wakeup_time(timer_id, ticks) +/** + * @brief Clear/disarm the wakeup timer alarm + * + * Disables the comparator left armed by rtc_timer_hal_set_wakeup_time() and + * clears any pending alarm interrupt status. + * + * @param timer_id Timer ID + */ +#define rtc_timer_hal_clear_wakeup_time(timer_id) rtc_timer_ll_clear_wakeup_time(timer_id) + /** * @brief Get current RTC timer cycle count * diff --git a/components/esp_hal_rtc_timer/test_apps/main/test_rtc_timer.c b/components/esp_hal_rtc_timer/test_apps/main/test_rtc_timer.c index 0906a9523d7..262088de301 100644 --- a/components/esp_hal_rtc_timer/test_apps/main/test_rtc_timer.c +++ b/components/esp_hal_rtc_timer/test_apps/main/test_rtc_timer.c @@ -102,7 +102,6 @@ static void assert_alarm_interrupt_round_trip(uint8_t timer_id) uint64_t delay_ticks = rtc_time_us_to_slowclk(RTC_TIMER_ALARM_DELAY_US, period); const uint32_t raw_mask = get_alarm_intr_mask(timer_id); - rtc_timer_ll_set_target_enable(&LP_TIMER, timer_id, false); rtc_timer_ll_alarm_intr_enable(&LP_TIMER, timer_id, false); rtc_timer_ll_clear_alarm_intr_status(&LP_TIMER, timer_id); TEST_ASSERT_EQUAL_HEX32(0, rtc_timer_ll_get_intr_raw(&LP_TIMER, timer_id) & raw_mask); From 306d54b6d52ba41475630fdd73ef38b82e940cb7 Mon Sep 17 00:00:00 2001 From: wuzhenghui Date: Wed, 24 Jun 2026 11:50:40 +0800 Subject: [PATCH 2/4] feat(int_wdt): add pause and resume functions for cpu1 interrupt watchdog --- .../include/esp_private/esp_int_wdt.h | 30 +++++++++++++++++++ components/esp_system/int_wdt.c | 27 ++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/components/esp_system/include/esp_private/esp_int_wdt.h b/components/esp_system/include/esp_private/esp_int_wdt.h index 7166b2e2f05..a728e92c902 100644 --- a/components/esp_system/include/esp_private/esp_int_wdt.h +++ b/components/esp_system/include/esp_private/esp_int_wdt.h @@ -6,6 +6,7 @@ #pragma once +#include #include "sdkconfig.h" #ifdef __cplusplus @@ -36,6 +37,35 @@ void esp_int_wdt_init(void); */ void esp_int_wdt_cpu_init(void); +#if CONFIG_ESP_INT_WDT +/** + * @brief Pause (disable) the interrupt watchdog. + * + * @note Must be balanced by esp_int_wdt_resume(). Not nestable. + */ +void esp_int_wdt_pause(void); + +/** + * @brief Resume (re-enable and feed) the interrupt watchdog previously paused with esp_int_wdt_pause(). + */ +void esp_int_wdt_resume(void); + +#if CONFIG_ESP_INT_WDT_CHECK_CPU1 +/** + * @brief Pause or resume INT WDT CPU1 liveness checking. + * + * Distinct from esp_int_wdt_pause(): this does not disable the watchdog. + * With ESP_INT_WDT_CHECK_CPU1 enabled, CPU0 only feeds the watchdog after CPU1 has also ticked. + * When CPU1 is intentionally idle and no longer ticks (e.g. tickless WAITI). + * Call with pause=true so CPU0 can keep feeding (and stay protected). + * Call with pause=false when CPU1 is active again. + * + * @param pause true to pause the CPU1 liveness check, false to resume it + */ +void esp_int_wdt_pause_cpu1_checking(bool pause); +#endif // CONFIG_ESP_INT_WDT_CHECK_CPU1 +#endif // CONFIG_ESP_INT_WDT + #if CONFIG_ESP_INT_WDT && CONFIG_ESP32_ECO3_CACHE_LOCK_FIX /** * @brief Reconfigure WDT stage timeouts (ticks). diff --git a/components/esp_system/int_wdt.c b/components/esp_system/int_wdt.c index 8df87b8d095..b996be5a42e 100644 --- a/components/esp_system/int_wdt.c +++ b/components/esp_system/int_wdt.c @@ -114,6 +114,9 @@ void ESP_SYSTEM_IRAM_ATTR esp_int_wdt_reconfigure_ticks(uint32_t stage0_ticks, u #if CONFIG_ESP_INT_WDT_CHECK_CPU1 volatile bool int_wdt_cpu1_ticked = false; +/* Set while CPU1 is intentionally idle (e.g. tickless idle) and therefore not ticking. The CPU1 + * liveness requirement is bypassed so CPU0 keeps feeding the watchdog (and stays protected). */ +static volatile bool s_int_wdt_cpu1_idle = false; #endif static void ESP_SYSTEM_IRAM_ATTR tick_hook(void) @@ -125,7 +128,7 @@ static void ESP_SYSTEM_IRAM_ATTR tick_hook(void) return; } #if CONFIG_ESP_INT_WDT_CHECK_CPU1 - if (int_wdt_cpu1_ticked) { + if (int_wdt_cpu1_ticked || s_int_wdt_cpu1_idle) { int_wdt_cpu1_ticked = false; } else { return; @@ -191,4 +194,26 @@ void esp_int_wdt_cpu_init(void) esp_intr_enable_source(ETS_INT_WDT_INUM); } +void ESP_SYSTEM_IRAM_ATTR esp_int_wdt_pause(void) +{ + wdt_hal_write_protect_disable(&iwdt_context); + wdt_hal_disable(&iwdt_context); + wdt_hal_write_protect_enable(&iwdt_context); +} + +void ESP_SYSTEM_IRAM_ATTR esp_int_wdt_resume(void) +{ + wdt_hal_write_protect_disable(&iwdt_context); + wdt_hal_feed(&iwdt_context); + wdt_hal_enable(&iwdt_context); + wdt_hal_write_protect_enable(&iwdt_context); +} + +#if CONFIG_ESP_INT_WDT_CHECK_CPU1 +void ESP_SYSTEM_IRAM_ATTR esp_int_wdt_pause_cpu1_checking(bool pause) +{ + s_int_wdt_cpu1_idle = pause; +} +#endif // CONFIG_ESP_INT_WDT_CHECK_CPU1 + #endif // CONFIG_ESP_INT_WDT From 0aa75e183b84b57a05260ed0c77b57677a81c114 Mon Sep 17 00:00:00 2001 From: wuzhenghui Date: Tue, 14 Jul 2026 11:46:23 +0800 Subject: [PATCH 3/4] 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", From 16a59e04fe46ac8bea45322f42dea7c3d156a2ca Mon Sep 17 00:00:00 2001 From: wuzhenghui Date: Fri, 26 Jun 2026 10:26:10 +0800 Subject: [PATCH 4/4] feat(esp_pm): implement tickless idle test for WAITI mode --- .../esp_pm/test_apps/.build-test-rules.yml | 2 + .../test_apps/esp_pm/main/CMakeLists.txt | 4 + .../esp_pm/main/test_pm_tickless_idle.c | 456 ++++++++++++++++++ .../esp_pm/test_apps/esp_pm/pytest_esp_pm.py | 15 + .../esp_pm/sdkconfig.ci.tickless_waiti | 3 + 5 files changed, 480 insertions(+) create mode 100644 components/esp_pm/test_apps/esp_pm/main/test_pm_tickless_idle.c create mode 100644 components/esp_pm/test_apps/esp_pm/sdkconfig.ci.tickless_waiti diff --git a/components/esp_pm/test_apps/.build-test-rules.yml b/components/esp_pm/test_apps/.build-test-rules.yml index bed18a701bc..8bfc3975ef8 100644 --- a/components/esp_pm/test_apps/.build-test-rules.yml +++ b/components/esp_pm/test_apps/.build-test-rules.yml @@ -5,6 +5,8 @@ components/esp_pm/test_apps: - if: INCLUDE_DEFAULT == 1 disable: - if: CONFIG_NAME == "pm_pd_top_sleep" and IDF_TARGET not in ["esp32c5", "esp32c6", "esp32h2", "esp32p4"] + - if: CONFIG_NAME == "tickless_waiti" and IDF_TARGET in ["esp32", "esp32s2"] + reason: ESP32 & ESP32S2 uses CCOUNT systick, PM_TICKLESS_IDLE_WAITI requires SYSTIMER - if: IDF_TARGET in ["esp32h21", "esp32h4"] temporary: true reason: not support yet # TODO: [ESP32H21] IDF-11522, [ESP32H4] IDF-12286 diff --git a/components/esp_pm/test_apps/esp_pm/main/CMakeLists.txt b/components/esp_pm/test_apps/esp_pm/main/CMakeLists.txt index 38ec64772db..a006dab044b 100644 --- a/components/esp_pm/test_apps/esp_pm/main/CMakeLists.txt +++ b/components/esp_pm/test_apps/esp_pm/main/CMakeLists.txt @@ -1,6 +1,10 @@ set(sources "test_app_main.c" "test_pm.c") +if(CONFIG_PM_TICKLESS_IDLE_WAITI) + list(APPEND sources "test_pm_tickless_idle.c") +endif() + if(CONFIG_SOC_CPU_HAS_FPU AND CONFIG_IDF_TARGET_ARCH_RISCV AND CONFIG_SOC_PM_FPU_RETENTION_BY_SW) list(APPEND sources "test_fpu_retention.c") endif() diff --git a/components/esp_pm/test_apps/esp_pm/main/test_pm_tickless_idle.c b/components/esp_pm/test_apps/esp_pm/main/test_pm_tickless_idle.c new file mode 100644 index 00000000000..1948d4f412e --- /dev/null +++ b/components/esp_pm/test_apps/esp_pm/main/test_pm_tickless_idle.c @@ -0,0 +1,456 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include "sdkconfig.h" + +#if CONFIG_PM_ENABLE && CONFIG_FREERTOS_USE_TICKLESS_IDLE && CONFIG_PM_TICKLESS_IDLE_WAITI + +#include "unity.h" +#include "esp_pm.h" +#include "esp_timer.h" +#include "esp_random.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_private/esp_clk.h" +#include "esp_private/esp_sleep_internal.h" + +#define MHZ (1000 * 1000) + +#define TICKLESS_WAITI_TICK_TOLERANCE 2 +/* 2 tick + duration * 5% (same as test_pm.c). */ +#define TICKLESS_IDLE_TICK_TOLERANCE(expected_ticks) \ + (TICKLESS_WAITI_TICK_TOLERANCE + (expected_ticks) / 20) + +static void tickless_waiti_configure_pm(void) +{ + int cur_freq_mhz = esp_clk_cpu_freq() / MHZ; + int xtal_freq = esp_clk_xtal_freq() / MHZ; + + esp_pm_config_t pm_config = { + .max_freq_mhz = cur_freq_mhz, + .min_freq_mhz = xtal_freq, + .light_sleep_enable = false, + }; + ESP_ERROR_CHECK(esp_pm_configure(&pm_config)); +} + +static void tickless_waiti_configure_pm_light_sleep(void) +{ + int cur_freq_mhz = esp_clk_cpu_freq() / MHZ; + int xtal_freq = esp_clk_xtal_freq() / MHZ; + + esp_pm_config_t pm_config = { + .max_freq_mhz = cur_freq_mhz, + .min_freq_mhz = xtal_freq, + .light_sleep_enable = true, + }; + ESP_ERROR_CHECK(esp_pm_configure(&pm_config)); +} + +static void tickless_waiti_restore_pm(void) +{ + int cur_freq_mhz = esp_clk_cpu_freq() / MHZ; + + esp_pm_config_t pm_config = { + .max_freq_mhz = cur_freq_mhz, + .min_freq_mhz = cur_freq_mhz, + }; + ESP_ERROR_CHECK(esp_pm_configure(&pm_config)); +} + +static void tickless_waiti_assert_vtaskdelay_tol(int delay_ms, int tick_tol) +{ + vTaskDelay(1); + + int64_t start_us = esp_timer_get_time(); + TickType_t start_tick = xTaskGetTickCount(); + vTaskDelay(pdMS_TO_TICKS(delay_ms)); + TickType_t end_tick = xTaskGetTickCount(); + int64_t elapsed_us = esp_timer_get_time() - start_us; + + int expected_ticks = pdMS_TO_TICKS(delay_ms); + int actual_ticks = (int)(end_tick - start_tick); + int time_tol_us = tick_tol * portTICK_PERIOD_MS * 1000; + + printf("delay=%dms elapsed=%dms ticks=%d(expected %d) tol=%d\n", + delay_ms, (int)(elapsed_us / 1000), actual_ticks, expected_ticks, tick_tol); + + TEST_ASSERT_INT_WITHIN(tick_tol, expected_ticks, actual_ticks); + TEST_ASSERT_INT32_WITHIN(time_tol_us, delay_ms * 1000, (int32_t)elapsed_us); +} + +static void tickless_waiti_assert_vtaskdelay(int delay_ms) +{ + int expected_ticks = pdMS_TO_TICKS(delay_ms); + tickless_waiti_assert_vtaskdelay_tol(delay_ms, TICKLESS_IDLE_TICK_TOLERANCE(expected_ticks)); +} + +#if CONFIG_ESP_SLEEP_DEBUG +static void tickless_waiti_assert_light_sleep_ok(esp_sleep_context_t *sleep_ctx, uint32_t ls_cnt_before) +{ + printf("lightsleep_cnt=%lu (was %lu) sleep_request_result=%d\n", + (unsigned long)sleep_ctx->lightsleep_cnt, (unsigned long)ls_cnt_before, + (int)sleep_ctx->sleep_request_result); + TEST_ASSERT_GREATER_THAN(ls_cnt_before, sleep_ctx->lightsleep_cnt); + TEST_ASSERT_EQUAL(ESP_OK, sleep_ctx->sleep_request_result); +} + +static void tickless_waiti_assert_no_light_sleep(esp_sleep_context_t *sleep_ctx, uint32_t ls_cnt_before) +{ + TEST_ASSERT_EQUAL(ls_cnt_before, sleep_ctx->lightsleep_cnt); +} +#endif + +typedef struct { + int delay_ms; + int loop_count; + int tick_tol; + int tick_errors; + int early_wake_errors; + SemaphoreHandle_t done; +} tickless_waiti_core_delay_arg_t; + +static void tickless_waiti_core_delay_task(void *arg) +{ + tickless_waiti_core_delay_arg_t *params = (tickless_waiti_core_delay_arg_t *)arg; + const int expected_ticks = pdMS_TO_TICKS(params->delay_ms); + const int core_id = xPortGetCoreID(); + + vTaskDelay((esp_random() % 5) + 1); + + for (int loop = 0; loop < params->loop_count; loop++) { + if (loop > 0) { + vTaskDelay(esp_random() % 3); + } + + vTaskDelay(1); + + TickType_t start_tick = xTaskGetTickCount(); + vTaskDelay(pdMS_TO_TICKS(params->delay_ms)); + int actual_ticks = (int)(xTaskGetTickCount() - start_tick); + + if (actual_ticks < expected_ticks - params->tick_tol) { + params->early_wake_errors++; + printf("core%d loop=%d early wake delay=%dms ticks=%d(expected %d)\n", + core_id, loop, params->delay_ms, actual_ticks, expected_ticks); + } else if (actual_ticks > expected_ticks + params->tick_tol) { + params->tick_errors++; + printf("core%d loop=%d late delay=%dms ticks=%d(expected %d)\n", + core_id, loop, params->delay_ms, actual_ticks, expected_ticks); + } + + xSemaphoreGive(params->done); + } + + vTaskDelete(NULL); +} + +/* Pin idle work to core0; on dual-core also create a core1 task. */ +static void tickless_waiti_run_core_delays(int delay0_ms, int loops0, int delay1_ms, int loops1, int tick_tol) +{ + SemaphoreHandle_t done0 = xSemaphoreCreateCounting(loops0, 0); + TEST_ASSERT_NOT_NULL(done0); + + tickless_waiti_core_delay_arg_t arg0 = { + .delay_ms = delay0_ms, + .loop_count = loops0, + .tick_tol = tick_tol, + .done = done0, + }; + + TEST_ASSERT_EQUAL(pdPASS, xTaskCreatePinnedToCore( + tickless_waiti_core_delay_task, "waiti0", 4096, &arg0, + tskIDLE_PRIORITY + 2, NULL, 0)); + +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + SemaphoreHandle_t done1 = xSemaphoreCreateCounting(loops1, 0); + TEST_ASSERT_NOT_NULL(done1); + + tickless_waiti_core_delay_arg_t arg1 = { + .delay_ms = delay1_ms, + .loop_count = loops1, + .tick_tol = tick_tol, + .done = done1, + }; + + TEST_ASSERT_EQUAL(pdPASS, xTaskCreatePinnedToCore( + tickless_waiti_core_delay_task, "waiti1", 4096, &arg1, + tskIDLE_PRIORITY + 2, NULL, 1)); +#else + (void)delay1_ms; + (void)loops1; +#endif + + int pending0 = loops0; +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + int pending1 = loops1; +#endif + int wait_loops = 0; + const int max_wait_loops = loops0 + loops1 + 100; + + while (pending0 > 0 +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + || pending1 > 0 +#endif + ) { + TEST_ASSERT_LESS_THAN(max_wait_loops, wait_loops++); + + if (pending0 > 0 && xSemaphoreTake(done0, pdMS_TO_TICKS(50)) == pdTRUE) { + pending0--; + continue; + } +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + if (pending1 > 0 && xSemaphoreTake(done1, pdMS_TO_TICKS(50)) == pdTRUE) { + pending1--; + continue; + } +#endif + } + + TEST_ASSERT_EQUAL(0, arg0.tick_errors); + TEST_ASSERT_EQUAL(0, arg0.early_wake_errors); +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + TEST_ASSERT_EQUAL(0, arg1.tick_errors); + TEST_ASSERT_EQUAL(0, arg1.early_wake_errors); + vSemaphoreDelete(done1); +#endif + vSemaphoreDelete(done0); +} + +/* + * Verify: tickless WAITI suppress + LP alarm + xTaskCatchUpTicks keep RTOS tick and + * esp_timer wall time aligned with the requested delay. + * light_sleep_enable=false forces the WAITI path; sample delays at the tickless + * threshold, a short multi-tick park, and a long park that needs multi-tick catch-up. + */ +TEST_CASE("Tickless WAITI tick compensation is accurate", "[pm]") +{ + tickless_waiti_configure_pm(); + + const int idle_ms = CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP * portTICK_PERIOD_MS; + const int delays_ms[] = { + idle_ms, + 5 * idle_ms, + 50 * idle_ms, + 100 * idle_ms, + 300 * idle_ms, + }; + + for (int i = 0; i < sizeof(delays_ms) / sizeof(delays_ms[0]); i++) { + tickless_waiti_assert_vtaskdelay(delays_ms[i]); + } + + tickless_waiti_restore_pm(); +} + +/* + * Verify: with light_sleep_enable=true, ESP_PM_NO_LIGHT_SLEEP still selects WAITI (not LS) + * and tick compensation remains correct. + * acquire NO_LIGHT_SLEEP; assert lightsleep_cnt does not advance across idle; assert + * RTOS tick / wall time within WAITI tolerance. + */ +TEST_CASE("Tickless WAITI fallback when light sleep blocked by PM lock", "[pm]") +{ + tickless_waiti_configure_pm_light_sleep(); + + esp_sleep_context_t sleep_ctx = {}; + esp_sleep_set_sleep_context(&sleep_ctx); + + esp_pm_lock_handle_t lock; + TEST_ESP_OK(esp_pm_lock_create(ESP_PM_NO_LIGHT_SLEEP, 0, "waiti_lock", &lock)); + TEST_ESP_OK(esp_pm_lock_acquire(lock)); + + const int delay_ms = 20 * CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP * portTICK_PERIOD_MS; + uint32_t ls_cnt = sleep_ctx.lightsleep_cnt; + tickless_waiti_assert_vtaskdelay(delay_ms); + tickless_waiti_assert_no_light_sleep(&sleep_ctx, ls_cnt); + + TEST_ESP_OK(esp_pm_lock_release(lock)); + TEST_ESP_OK(esp_pm_lock_delete(lock)); + esp_sleep_set_sleep_context(NULL); + tickless_waiti_restore_pm(); +} + +/* + * Verify: WAITI and esp_pm auto light sleep can alternate without breaking idle timing or + * leaving the RTC/LP comparator in a bad state for the next WAITI. + * + * 1) Auto LS idle — lightsleep_cnt increases and sleep_request_result == ESP_OK; tick within + * light-sleep tolerance (dual-core: both cores idle). + * 2) Switch to WAITI-only (light_sleep_enable=false) immediately after LS — reclaim path; tick + * within WAITI tolerance and lightsleep_cnt unchanged. + * 3) light_sleep_enable=true again; release/acquire NO_LIGHT_SLEEP to toggle LS <-> WAITI a few + * times (fallback accuracy alone is in the lock case above). + */ +TEST_CASE("Tickless WAITI coexists with esp_pm auto light sleep", "[pm]") +{ +#if !CONFIG_ESP_SLEEP_DEBUG + TEST_IGNORE_MESSAGE("requires CONFIG_ESP_SLEEP_DEBUG"); +#else + const int delay_ms = 10 * CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP * portTICK_PERIOD_MS; + const int ls_tol = TICKLESS_IDLE_TICK_TOLERANCE(pdMS_TO_TICKS(delay_ms)); + + esp_sleep_context_t sleep_ctx = {}; + esp_sleep_set_sleep_context(&sleep_ctx); + + /* 1) Auto light sleep succeeds */ + tickless_waiti_configure_pm_light_sleep(); + uint32_t ls_cnt = sleep_ctx.lightsleep_cnt; + tickless_waiti_run_core_delays(delay_ms, 2, delay_ms + 40, 2, ls_tol); + tickless_waiti_assert_light_sleep_ok(&sleep_ctx, ls_cnt); + + /* 2) WAITI reclaim right after LS armed the RTC/LP timer */ + tickless_waiti_configure_pm(); + ls_cnt = sleep_ctx.lightsleep_cnt; + tickless_waiti_run_core_delays(delay_ms, 2, delay_ms + 40, 2, TICKLESS_WAITI_TICK_TOLERANCE); + tickless_waiti_assert_no_light_sleep(&sleep_ctx, ls_cnt); + + /* 3) Toggle LS <-> WAITI under light_sleep_enable=true */ + tickless_waiti_configure_pm_light_sleep(); + esp_pm_lock_handle_t no_ls_lock; + TEST_ESP_OK(esp_pm_lock_create(ESP_PM_NO_LIGHT_SLEEP, 0, "waiti_coexist", &no_ls_lock)); + + for (int round = 0; round < 3; round++) { + printf("coexist toggle round %d\n", round); + + ls_cnt = sleep_ctx.lightsleep_cnt; + tickless_waiti_run_core_delays(delay_ms, 2, delay_ms + 30, 2, ls_tol); + tickless_waiti_assert_light_sleep_ok(&sleep_ctx, ls_cnt); + + TEST_ESP_OK(esp_pm_lock_acquire(no_ls_lock)); + ls_cnt = sleep_ctx.lightsleep_cnt; + tickless_waiti_run_core_delays(delay_ms, 2, delay_ms + 30, 2, TICKLESS_WAITI_TICK_TOLERANCE); + tickless_waiti_assert_no_light_sleep(&sleep_ctx, ls_cnt); + TEST_ESP_OK(esp_pm_lock_release(no_ls_lock)); + } + + TEST_ESP_OK(esp_pm_lock_delete(no_ls_lock)); + esp_sleep_set_sleep_context(NULL); + tickless_waiti_restore_pm(); +#endif /* CONFIG_ESP_SLEEP_DEBUG */ +} + +typedef struct { + SemaphoreHandle_t fired; + int64_t first_fire_us; + int64_t start_time_us; + int fire_count; +} tickless_waiti_timer_arg_t; + +static void tickless_waiti_timer_cb(void *arg) +{ + tickless_waiti_timer_arg_t *ctx = (tickless_waiti_timer_arg_t *)arg; + int64_t now = esp_timer_get_time(); + + if (ctx->fire_count == 0) { + ctx->first_fire_us = now; + } + ctx->fire_count++; + xSemaphoreGive(ctx->fired); +} + +/* + * Verify: non-LP interrupts (esp_timer) can wake WAITI early / repeatedly, and tick catch-up + * still matches the full vTaskDelay. + * + * A) One-shot timer before the delay ends — proves early exit from WFI + Claim/CatchUp still + * delivers the full delay length. + * B) Periodic timer across a long idle — proves repeated wakeups do not accumulate tick error. + */ +TEST_CASE("Tickless WAITI esp_timer wakeup during idle", "[pm]") +{ + tickless_waiti_configure_pm(); + + tickless_waiti_timer_arg_t ctx = {0}; + ctx.fired = xSemaphoreCreateCounting(64, 0); + TEST_ASSERT_NOT_NULL(ctx.fired); + + esp_timer_handle_t timer; + esp_timer_create_args_t create_args = { + .callback = tickless_waiti_timer_cb, + .arg = &ctx, + .name = "waiti_timer", + }; + TEST_ESP_OK(esp_timer_create(&create_args, &timer)); + + /* --- A) One-shot early wake --- */ + const int early_delay_ms = 200; + const int oneshot_ms = 50; + ctx.fire_count = 0; + ctx.first_fire_us = 0; + TEST_ESP_OK(esp_timer_start_once(timer, oneshot_ms * 1000ULL)); + vTaskDelay(1); + ctx.start_time_us = esp_timer_get_time(); + tickless_waiti_assert_vtaskdelay(early_delay_ms); + /* One-shot already finished; stop is optional and may return INVALID_STATE. */ + (void)esp_timer_stop(timer); + + int first_fire_ms = (int)((ctx.first_fire_us - ctx.start_time_us) / 1000); + printf("oneshot: first_fire=%dms (expect ~%d) fires=%d\n", + first_fire_ms, oneshot_ms, ctx.fire_count); + TEST_ASSERT_EQUAL(1, ctx.fire_count); + TEST_ASSERT_INT32_WITHIN(15, oneshot_ms, first_fire_ms); + + /* --- B) Periodic wakes during long idle --- */ + const int delay_ms = 500; + const int period_ms = 25; + const int min_fires = (delay_ms / period_ms) - 3; + ctx.fire_count = 0; + ctx.first_fire_us = 0; + while (xSemaphoreTake(ctx.fired, 0) == pdTRUE) { + } + + TEST_ESP_OK(esp_timer_start_periodic(timer, period_ms * 1000ULL)); + vTaskDelay(1); + ctx.start_time_us = esp_timer_get_time(); + tickless_waiti_assert_vtaskdelay(delay_ms); + TEST_ESP_OK(esp_timer_stop(timer)); + + first_fire_ms = (int)((ctx.first_fire_us - ctx.start_time_us) / 1000); + printf("periodic: first_fire=%dms period=%dms fires=%d during %dms\n", + first_fire_ms, period_ms, ctx.fire_count, delay_ms); + TEST_ASSERT_GREATER_OR_EQUAL(min_fires, ctx.fire_count); + TEST_ASSERT_INT32_WITHIN(15, period_ms, first_fire_ms); + + TEST_ESP_OK(esp_timer_delete(timer)); + vSemaphoreDelete(ctx.fired); + tickless_waiti_restore_pm(); +} + +#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1 + +/* + * Verify: dual-core WAITI with asymmetric idle windows (shared LP comparator partial disarm, + * per-core tick suppress, INT_WDT CPU1-idle bypass) keeps each core's delays accurate. + * light_sleep_enable=false; core0 short/frequent parks, core1 long/rare parks with + * random phase so WAITI windows cross; assert no early/late tick errors on either core. + * (LS <-> WAITI dual-core switching is in the coexist case.) + */ +TEST_CASE("Tickless WAITI dual-core asymmetric idle (partial LP disarm)", "[pm]") +{ + tickless_waiti_configure_pm(); + + const int delay0_ms = 40; + const int loop_count0 = 12; + const int delay1_ms = 180; + const int loop_count1 = 4; + + printf("asymmetric idle: core0 %dms x %d loops, core1 %dms x %d loops\n", + delay0_ms, loop_count0, delay1_ms, loop_count1); + + tickless_waiti_run_core_delays(delay0_ms, loop_count0, delay1_ms, loop_count1, + TICKLESS_WAITI_TICK_TOLERANCE); + + vTaskDelay(1); + tickless_waiti_restore_pm(); +} + +#endif /* CONFIG_FREERTOS_NUMBER_OF_CORES > 1 */ + +#endif /* CONFIG_PM_ENABLE && CONFIG_FREERTOS_USE_TICKLESS_IDLE && CONFIG_PM_TICKLESS_IDLE_WAITI */ diff --git a/components/esp_pm/test_apps/esp_pm/pytest_esp_pm.py b/components/esp_pm/test_apps/esp_pm/pytest_esp_pm.py index 9b86e2325e3..7f6deea04fd 100644 --- a/components/esp_pm/test_apps/esp_pm/pytest_esp_pm.py +++ b/components/esp_pm/test_apps/esp_pm/pytest_esp_pm.py @@ -51,7 +51,22 @@ def test_esp_attr_xip_psram_esp32s3(dut: Dut) -> None: @pytest.mark.parametrize( 'config', ['pm_pd_top_sleep'], + indirect=True, ) @idf_parametrize('target', ['esp32c5', 'esp32c6', 'esp32h2', 'esp32p4'], indirect=['target']) def test_esp_pd_top_and_cpu_sleep(dut: Dut) -> None: dut.run_all_single_board_cases() + + +# Tickless IDLE without lightsleep +@pytest.mark.generic +@pytest.mark.parametrize( + 'config', + ['tickless_waiti'], + indirect=True, +) +@idf_parametrize('target', ['supported_targets'], indirect=['target']) +@pytest.mark.temp_skip_ci(targets=['esp32', 'esp32s2'], reason='PM_TICKLESS_IDLE_WAITI requires systimer') +@pytest.mark.temp_skip_ci(targets=['esp32h4'], reason='bringup on this module is not done') +def test_ticlkess_waiti(dut: Dut) -> None: + dut.run_all_single_board_cases() diff --git a/components/esp_pm/test_apps/esp_pm/sdkconfig.ci.tickless_waiti b/components/esp_pm/test_apps/esp_pm/sdkconfig.ci.tickless_waiti new file mode 100644 index 00000000000..54000f37235 --- /dev/null +++ b/components/esp_pm/test_apps/esp_pm/sdkconfig.ci.tickless_waiti @@ -0,0 +1,3 @@ +CONFIG_PM_TICKLESS_IDLE_WAITI=y +CONFIG_FREERTOS_HZ=1000 +CONFIG_ESP_SLEEP_DEBUG=y