/** * @file c6_lp_core.c * @brief LP-core wake-on-motion hibernation — ADR-110 Phase 5 (full). * * Two operating modes, controlled by CONFIG_C6_LP_CORE_ENABLE: * * 1. ENABLED — real LP-core RISC-V program polls the wake GPIO at * LP_TIMER cadence (default 10 ms), debounces N matching samples, * and triggers an HP wake via `ulp_lp_core_wakeup_main_processor()`. * HP enters deep sleep with `ESP_SLEEP_WAKEUP_ULP` as the source. * Targets ~5 µA average current (datasheet figure for LP-core + * RTC peripherals powered down). The LP binary is built by * `ulp_embed_binary(...)` in main/CMakeLists.txt from lp_core/main.c. * * 2. DISABLED — falls back to plain deep-sleep + GPIO wake-up * (`esp_deep_sleep_enable_gpio_wakeup`). No debounce, no * sub-10 µA floor, but no LP toolchain dependency either. * This is the path the v0.6.6 firmware shipped with. * * Both paths share `c6_lp_core_arm()` / `c6_lp_core_hibernate_and_wait()` * so call sites in main.c don't change between modes. */ #include "sdkconfig.h" #if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_ULP_COPROC_TYPE_LP_CORE) #include "c6_lp_core.h" #include "esp_log.h" #include "esp_sleep.h" #include "driver/rtc_io.h" #include "soc/soc_caps.h" #include #if defined(CONFIG_C6_LP_CORE_ENABLE) #include "ulp_lp_core.h" /* ulp_main.h is auto-generated by `ulp_embed_binary(ulp_main, ...)` and * exports every `volatile` global from lp_core/main.c with the `ulp_` * prefix. Include is guarded so disabled builds don't try to find a * file the build system hasn't generated. */ #include "ulp_main.h" extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_main_bin_start"); extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_main_bin_end"); #endif static const char *TAG = "c6_lp"; static int s_wake_gpio = -1; static bool s_active_high = true; static bool s_armed = false; #ifndef CONFIG_C6_LP_POLL_PERIOD_US #define CONFIG_C6_LP_POLL_PERIOD_US 10000 /* 100 Hz default poll cadence */ #endif #ifndef CONFIG_C6_LP_DEBOUNCE_SAMPLES #define CONFIG_C6_LP_DEBOUNCE_SAMPLES 3 #endif esp_err_t c6_lp_core_arm(int wake_gpio, bool active_high) { if (wake_gpio < 0) { ESP_LOGE(TAG, "invalid wake_gpio=%d", wake_gpio); return ESP_ERR_INVALID_ARG; } s_wake_gpio = wake_gpio; s_active_high = active_high; /* GPIO must be in the LP/RTC domain for either wake path. */ esp_err_t ret = rtc_gpio_init(wake_gpio); if (ret != ESP_OK) { ESP_LOGE(TAG, "rtc_gpio_init(%d) failed: %s", wake_gpio, esp_err_to_name(ret)); return ret; } rtc_gpio_set_direction(wake_gpio, RTC_GPIO_MODE_INPUT_ONLY); /* Floating inputs in deep sleep are an antenna — disable internal pulls * only if the user has an external pull on the motion line; we leave * default pulls so a disconnected pin doesn't toggle randomly. */ #if defined(CONFIG_C6_LP_CORE_ENABLE) /* --- Real LP-core path --- */ /* On C6, LP-IO maps 1:1 to GPIO for indices 0..7. Validate. */ if (wake_gpio > 7) { ESP_LOGE(TAG, "LP-core path requires LP-IO 0..7, got GPIO %d", wake_gpio); return ESP_ERR_INVALID_ARG; } /* Load the LP-core binary blob. */ esp_err_t err = ulp_lp_core_load_binary( ulp_main_bin_start, (size_t)(ulp_main_bin_end - ulp_main_bin_start)); if (err != ESP_OK) { ESP_LOGE(TAG, "ulp_lp_core_load_binary failed: %s", esp_err_to_name(err)); return err; } /* Hand the GPIO parameters to the LP program via shared symbols. * These are declared `volatile` in lp_core/main.c so the HP write * is observed by LP on the next iteration. */ ulp_wake_gpio_num = (uint32_t)wake_gpio; ulp_wake_active_high = active_high ? 1u : 0u; ulp_debounce_samples = CONFIG_C6_LP_DEBOUNCE_SAMPLES; ulp_motion_count = 0; ulp_poll_count = 0; ulp_last_gpio_level = 0; /* Configure LP-timer wakeup at the configured poll period and start the * LP-core. `ulp_lp_core_run` is non-blocking; the LP core begins running * the program immediately and the HP core can proceed to deep sleep. */ ulp_lp_core_cfg_t cfg = { .wakeup_source = ULP_LP_CORE_WAKEUP_SOURCE_LP_TIMER, .lp_timer_sleep_duration_us = CONFIG_C6_LP_POLL_PERIOD_US, }; err = ulp_lp_core_run(&cfg); if (err != ESP_OK) { ESP_LOGE(TAG, "ulp_lp_core_run failed: %s", esp_err_to_name(err)); return err; } /* Tell deep-sleep that the LP-core is our wake source. */ err = esp_sleep_enable_ulp_wakeup(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_sleep_enable_ulp_wakeup failed: %s", esp_err_to_name(err)); return err; } s_armed = true; ESP_LOGI(TAG, "LP-core armed: gpio=%d active_%s debounce=%d poll=%d µs", wake_gpio, active_high ? "high" : "low", CONFIG_C6_LP_DEBOUNCE_SAMPLES, CONFIG_C6_LP_POLL_PERIOD_US); return ESP_OK; #else /* --- Fallback path: plain deep-sleep GPIO wakeup (~10 µA floor) --- */ uint64_t mask = 1ULL << wake_gpio; esp_deepsleep_gpio_wake_up_mode_t mode = active_high ? ESP_GPIO_WAKEUP_GPIO_HIGH : ESP_GPIO_WAKEUP_GPIO_LOW; esp_err_t err = esp_deep_sleep_enable_gpio_wakeup(mask, mode); if (err != ESP_OK) { ESP_LOGE(TAG, "enable_gpio_wakeup failed: %s", esp_err_to_name(err)); return err; } s_armed = true; ESP_LOGI(TAG, "GPIO-wakeup armed (no LP-core): gpio=%d active_%s", wake_gpio, active_high ? "high" : "low"); return ESP_OK; #endif } void c6_lp_core_hibernate_and_wait(void) { if (!s_armed) { ESP_LOGW(TAG, "hibernate called without arm — sleeping with no wake source"); } /* Power down the RTC peripheral domain — the LP-core itself stays * powered on the LP power domain so it can keep polling. */ esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF); #if defined(CONFIG_C6_LP_CORE_ENABLE) ESP_LOGI(TAG, "entering deep sleep — LP-core polling, target ≤5 µA"); #else ESP_LOGI(TAG, "entering deep sleep — GPIO wakeup, target ~10 µA"); #endif esp_deep_sleep_start(); /* Never returns. */ } bool c6_lp_core_was_motion_wake(void) { esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); #if defined(CONFIG_C6_LP_CORE_ENABLE) /* Real LP-core path: wakeup cause is ULP (LP-core triggered HP). */ if (cause == ESP_SLEEP_WAKEUP_ULP) return true; #endif /* Fallback path or alternate GPIO wakeup. */ return cause == ESP_SLEEP_WAKEUP_GPIO || cause == ESP_SLEEP_WAKEUP_EXT1; } #if defined(CONFIG_C6_LP_CORE_ENABLE) uint32_t c6_lp_core_motion_count(void) { return (uint32_t)ulp_motion_count; } uint32_t c6_lp_core_poll_count(void) { return (uint32_t)ulp_poll_count; } #else uint32_t c6_lp_core_motion_count(void) { return 0; } uint32_t c6_lp_core_poll_count(void) { return 0; } #endif #endif /* CONFIG_IDF_TARGET_ESP32C6 && CONFIG_ULP_COPROC_TYPE_LP_CORE */