Merge branch 'fix/lp_core_gpio_wakeup' into 'master'

fix(ulp): wake pin wakes ESP32-P4 from deep sleep on P4

Closes IDF-15823

See merge request espressif/esp-idf!49855
This commit is contained in:
Renz Christian Bagaporo
2026-07-10 07:44:36 +08:00
16 changed files with 386 additions and 5 deletions
@@ -231,6 +231,11 @@ static inline void ulp_lp_core_gpio_intr_enable(lp_io_num_t lp_io_num, gpio_int_
static inline void ulp_lp_core_gpio_clear_intr_status(void)
{
rtcio_ll_clear_interrupt_status();
#if SOC_RTC_GPIO_EDGE_WAKEUP_SUPPORTED
for (int rtcio_num = 0; rtcio_num < SOC_RTCIO_PIN_COUNT; rtcio_num++) {
rtcio_ll_clear_edge_wakeup_latch(rtcio_num);
}
#endif
}
/**
@@ -19,6 +19,17 @@ components/ulp/test_apps/lp_core/lp_core_basic_tests:
- esp_hw_support
components/ulp/test_apps/lp_core/lp_core_gpio_wakeup_tests:
enable:
- if: (SOC_LP_CORE_SUPPORTED == 1) and (SOC_RTCIO_PIN_COUNT > 0) and (SOC_DEEP_SLEEP_SUPPORTED == 1) and (SOC_RTC_GPIO_EDGE_WAKEUP_SUPPORTED == 1)
depends_components:
- ulp
- esp_driver_gpio
- esp_hal_gpio
- esp_hal_pmu
- esp_hw_support
components/ulp/test_apps/lp_core/lp_core_hp_mem:
disable:
- if: SOC_LP_CORE_SUPPORTED != 1
@@ -0,0 +1,14 @@
# This is the project CMakeLists.txt file for the test subproject
cmake_minimum_required(VERSION 3.22)
list(PREPEND SDKCONFIG_DEFAULTS "$ENV{IDF_PATH}/tools/test_apps/configs/sdkconfig.debug_helpers" "sdkconfig.defaults")
set(EXTRA_COMPONENT_DIRS
"$ENV{IDF_PATH}/tools/test_apps/components"
)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
set(COMPONENTS main)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(lp_core_gpio_wakeup_tests)
@@ -0,0 +1,31 @@
| Supported Targets | ESP32-C5 | ESP32-C6 | ESP32-P4 |
| ----------------- | -------- | -------- | -------- |
# LP Core GPIO Wakeup Tests
This test app verifies LP core GPIO wakeup from deep sleep using an **external GPIO toggle** provided by a second board.
It exercises the same flow as the `gpio_wakeup` example:
1. HP configures the wakeup GPIO, loads and starts the LP program, and enters deep sleep
2. The waker holds the GPIO at the idle high level long enough to catch spurious wakeups
3. An external falling edge wakes LP, which records the LP wakeup cause, wakes HP, and clears GPIO interrupt status
4. HP verifies the ULP wakeup cause and the LP GPIO wakeup cause
## Hardware setup
This is a `generic_multi_device` test. Two boards of the same target must have the configured wakeup GPIO pin wired together (default: GPIO2).
- **Wakee (DUT 0):** multi-stage test that enters deep sleep and verifies ULP wakeup after an external falling edge
- **Waker (DUT 1):** provides separate Unity test cases to drive the shared GPIO high or low
The wakee side uses `TEST_CASE_MULTIPLE_STAGES` because deep sleep reboots the chip back to the Unity menu; pytest reruns the after-wake stage once the board wakes up. Pytest drives the waker GPIO high before the wakee enters deep sleep, verifies that the wakee does not reboot early, and then drives the falling edge.
## Running locally
```bash
cd components/ulp/test_apps/lp_core/lp_core_gpio_wakeup_tests
idf.py set-target esp32p4
idf.py build
pytest --target esp32p4 -m generic_multi_device
```
@@ -0,0 +1,10 @@
set(app_sources "test_app_main.c" "test_lp_gpio_wakeup.c")
set(lp_core_sources "lp_core/test_main_gpio_wakeup.c")
idf_component_register(SRCS ${app_sources}
REQUIRES ulp unity test_utils esp_driver_gpio
WHOLE_ARCHIVE)
set(lp_core_exp_dep_srcs ${app_sources})
ulp_embed_binary(lp_core_test_app_gpio_wakeup "${lp_core_sources}" "${lp_core_exp_dep_srcs}")
@@ -0,0 +1,10 @@
menu "LP GPIO Wakeup Test Configuration"
config TEST_LP_GPIO_WAKEUP_PIN
int "GPIO pin wired between wakee and waker boards"
default 2
help
RTC GPIO pin used for LP core GPIO wakeup on the sleeping board.
The same pin number must be wired to the GPIO output on the waker board.
endmenu
@@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <stdbool.h>
#include "ulp_lp_core_utils.h"
#include "ulp_lp_core_gpio.h"
#include "hal/lp_core_ll.h"
uint32_t lp_wakeup_cause;
int main(void)
{
lp_wakeup_cause = ulp_lp_core_get_wakeup_cause();
ulp_lp_core_wakeup_main_processor();
ulp_lp_core_gpio_clear_intr_status();
return 0;
}
@@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "unity.h"
#include "unity_test_runner.h"
#include "esp_heap_caps.h"
// Some resources are lazy allocated in the sleep code, the threshold is left for that case
#define TEST_MEMORY_LEAK_THRESHOLD (-500)
static size_t before_free_8bit;
static size_t before_free_32bit;
static void check_leak(size_t before_free, size_t after_free, const char *type)
{
ssize_t delta = after_free - before_free;
printf("MALLOC_CAP_%s: Before %u bytes free, After %u bytes free (delta %d)\n", type, before_free, after_free, delta);
TEST_ASSERT_MESSAGE(delta >= TEST_MEMORY_LEAK_THRESHOLD, "memory leak");
}
void setUp(void)
{
before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT);
before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT);
}
void tearDown(void)
{
size_t after_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT);
size_t after_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT);
check_leak(before_free_8bit, after_free_8bit, "8BIT");
check_leak(before_free_32bit, after_free_32bit, "32BIT");
}
void app_main(void)
{
unity_run_menu();
}
@@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "unity.h"
#include "test_utils.h"
#include "esp_sleep.h"
#include "driver/rtc_io.h"
#include "driver/gpio.h"
#include "ulp_lp_core.h"
#include "hal/lp_core_ll.h"
#include "lp_core_test_app_gpio_wakeup.h"
#define TEST_WAKEUP_PIN CONFIG_TEST_LP_GPIO_WAKEUP_PIN
#define GPIO_IDLE_HIGH_SIGNAL "gpio idle high"
extern const uint8_t lp_core_gpio_wakeup_bin_start[] asm("_binary_lp_core_test_app_gpio_wakeup_bin_start");
extern const uint8_t lp_core_gpio_wakeup_bin_end[] asm("_binary_lp_core_test_app_gpio_wakeup_bin_end");
static void configure_wakee_gpio(void)
{
rtc_gpio_init(TEST_WAKEUP_PIN);
rtc_gpio_set_direction(TEST_WAKEUP_PIN, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_dis(TEST_WAKEUP_PIN);
rtc_gpio_pullup_en(TEST_WAKEUP_PIN);
rtc_gpio_wakeup_enable(TEST_WAKEUP_PIN, GPIO_INTR_NEGEDGE);
}
static void load_and_start_lp_core_program(void)
{
TEST_ESP_OK(ulp_lp_core_load_binary(lp_core_gpio_wakeup_bin_start,
lp_core_gpio_wakeup_bin_end - lp_core_gpio_wakeup_bin_start));
ulp_lp_core_cfg_t cfg = {
.wakeup_source = ULP_LP_CORE_WAKEUP_SOURCE_LP_IO,
};
TEST_ESP_OK(ulp_lp_core_run(&cfg));
}
static void configure_waker_gpio_output(void)
{
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << TEST_WAKEUP_PIN),
.mode = GPIO_MODE_OUTPUT,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.pull_up_en = GPIO_PULLUP_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
TEST_ESP_OK(gpio_config(&io_conf));
}
static void lp_gpio_wakeup_wakee_init(void)
{
configure_wakee_gpio();
load_and_start_lp_core_program();
unity_wait_for_signal(GPIO_IDLE_HIGH_SIGNAL);
TEST_ESP_OK(esp_sleep_enable_ulp_wakeup());
esp_deep_sleep_start();
TEST_FAIL_MESSAGE("Should have entered deep sleep");
}
static void lp_gpio_wakeup_wakee_after_wake(void)
{
TEST_ASSERT(esp_sleep_get_wakeup_causes() & BIT(ESP_SLEEP_WAKEUP_ULP));
printf("LP wakeup cause: 0x%" PRIx32 "\n", ulp_lp_wakeup_cause);
TEST_ASSERT_EQUAL_HEX32(LP_CORE_LL_WAKEUP_SOURCE_LP_IO, ulp_lp_wakeup_cause);
}
static void lp_gpio_wakeup_waker_idle_high(void)
{
configure_waker_gpio_output();
TEST_ESP_OK(gpio_set_level(TEST_WAKEUP_PIN, 1));
}
static void lp_gpio_wakeup_waker_wake_low(void)
{
configure_waker_gpio_output();
TEST_ESP_OK(gpio_set_level(TEST_WAKEUP_PIN, 0));
}
TEST_CASE_MULTIPLE_STAGES(
"LP GPIO wakeup wakee from deep sleep",
"[lp_core][lp_gpio_wakeup][test_env=generic_multi_device][timeout=120][deepsleep]",
lp_gpio_wakeup_wakee_init,
lp_gpio_wakeup_wakee_after_wake);
TEST_CASE(
"LP GPIO wakeup waker set idle high",
"[lp_core][lp_gpio_wakeup][test_env=generic_multi_device][timeout=120]")
{
lp_gpio_wakeup_waker_idle_high();
}
TEST_CASE(
"LP GPIO wakeup waker set wake low",
"[lp_core][lp_gpio_wakeup][test_env=generic_multi_device][timeout=120]")
{
lp_gpio_wakeup_waker_wake_low();
}
@@ -0,0 +1,116 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
from typing import TYPE_CHECKING
import pytest
from pexpect.exceptions import TIMEOUT
from pytest_embedded_idf import CaseTester
from pytest_embedded_idf import UnittestMenuCase
from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
if TYPE_CHECKING:
from pytest_embedded_idf.dut import IdfDut
WAKEE_DUT_INDEX = 0
WAKER_DUT_INDEX = 1
TEST_TIMEOUT = 120.0
NO_EARLY_WAKE_TIMEOUT = 2.0
LP_GPIO_WAKEUP_GROUP = 'lp_gpio_wakeup'
GPIO_IDLE_HIGH_SIGNAL = 'gpio idle high'
def _wait_signal_line(signal: str) -> str:
return f'Waiting for signal: [{signal}]!'
def _start_unity_case(dut: 'IdfDut', case: UnittestMenuCase, timeout: float, *, wait_ready: bool = True) -> None:
if wait_ready:
dut._get_ready(timeout)
dut.confirm_write(case.index, expect_str=f'Running {case.name}...')
def _run_unity_case(dut: 'IdfDut', case: UnittestMenuCase, timeout: float) -> None:
_start_unity_case(dut, case, timeout)
dut.expect_unity_test_output(timeout=timeout)
def _run_unity_stage(
dut: 'IdfDut',
case: UnittestMenuCase,
stage_index: int,
timeout: float,
*,
wait_ready: bool = True,
) -> None:
_start_unity_case(dut, case, timeout, wait_ready=wait_ready)
dut.write(str(stage_index))
def _find_single_test_case(
case_tester: CaseTester,
case_type: str,
name_fragment: str | None = None,
) -> UnittestMenuCase:
cases = [
case
for case in case_tester.test_menu
if case.type == case_type
and LP_GPIO_WAKEUP_GROUP in case.groups
and (name_fragment is None or name_fragment in case.name)
]
assert len(cases) == 1, f"Expected 1 {case_type} case with group '{LP_GPIO_WAKEUP_GROUP}', found {len(cases)}."
return cases[0]
@pytest.mark.generic_multi_device
@pytest.mark.parametrize('count', [2], indirect=True)
@pytest.mark.parametrize(
'config',
[
'defaults',
],
indirect=True,
)
@idf_parametrize(
'target',
soc_filtered_targets(
'(SOC_LP_CORE_SUPPORTED == 1) and '
'(SOC_RTCIO_PIN_COUNT > 0) and '
'(SOC_DEEP_SLEEP_SUPPORTED == 1) and '
'(SOC_RTC_GPIO_EDGE_WAKEUP_SUPPORTED == 1)'
),
indirect=['target'],
)
def test_lp_gpio_wakeup_from_deep_sleep(case_tester: CaseTester) -> None:
wakee_case = _find_single_test_case(case_tester, 'multi_stage')
waker_idle_case = _find_single_test_case(case_tester, 'normal', name_fragment='idle high')
waker_wake_case = _find_single_test_case(case_tester, 'normal', name_fragment='wake low')
assert len(wakee_case.subcases) == 2, f'Expected 2 wakee stages, found {len(wakee_case.subcases)}.'
wakee = case_tester.dut[WAKEE_DUT_INDEX]
waker = case_tester.dut[WAKER_DUT_INDEX]
wakee_init_stage, wakee_after_wake_stage = wakee_case.subcases
wakee.serial.hard_reset()
waker.serial.hard_reset()
_run_unity_case(waker, waker_idle_case, TEST_TIMEOUT)
_run_unity_stage(wakee, wakee_case, int(wakee_init_stage['index']), TEST_TIMEOUT)
wakee.expect_exact(_wait_signal_line(GPIO_IDLE_HIGH_SIGNAL), timeout=TEST_TIMEOUT)
wakee.write('')
try:
wakee._get_ready(NO_EARLY_WAKE_TIMEOUT)
except TIMEOUT:
pass
else:
pytest.fail('Wakee rebooted before the waker drove the GPIO falling edge')
_run_unity_case(waker, waker_wake_case, TEST_TIMEOUT)
wakee._get_ready(TEST_TIMEOUT)
_run_unity_stage(wakee, wakee_case, int(wakee_after_wake_stage['index']), TEST_TIMEOUT, wait_ready=False)
wakee.expect_unity_test_output(timeout=TEST_TIMEOUT)
@@ -0,0 +1 @@
# CI defaults for lp_core_gpio_wakeup_tests (see sdkconfig.defaults)
@@ -0,0 +1,5 @@
CONFIG_ESP_TASK_WDT_INIT=n
CONFIG_ULP_COPROC_ENABLED=y
CONFIG_ULP_COPROC_TYPE_LP_CORE=y
CONFIG_ULP_COPROC_RESERVE_MEM=12000