Root cause (verified against our exact IDF tree, not the community guess): the "258" in "sdio_write_task: Failed to send data: 258" is NOT a timeout (that is 263). 258 = 0x102 = ESP_ERR_INVALID_ARG. On the ESP32-P4, block- mode CMD53 writes require the SOURCE buffer to be 64-byte (cache-line) aligned; the IDF sdmmc driver rejects a misaligned source with INVALID_ARG BEFORE any bus activity. esp_hosts write loop then declares "Unrecoverable host sdio state" and reboots the whole P4. The audio TX payload is not 64-aligned, so streaming mic audio wedged on the very FIRST frame (which is exactly what we saw: listening -> instant Failed to send -> reboot). This also explains why buffer/queue/clock/retry tuning all did nothing: the write never reached the bus. And why our symptom was instant, not after ~100 writes (the community block-mode-desync theory) — it is the first misaligned buffer, every time. Fix: vendored esp_hosted 2.12.11 as an editable local component (overrides the registry copy) and bounce a misaligned TX payload through one aligned DMA scratch buffer in hosted_sdio_write_block (port_esp_hosted_host_sdio.c). TX is serialized by the bus lock so a single static bounce buffer is safe; freed in hosted_sdio_deinit. Host-only change — no C6 reflash. VERIFIED ON HARDWARE (autonomous self-test): 40s of continuous mic-audio upstream streaming — the traffic that previously wedged on the first frame — ran clean, zero timeouts, zero reboots. A guarded SDIO_TX_SELFTEST harness is kept (compiled out) for future SDIO stress testing. Credit: root cause + patch designed via multi-agent investigation; the precise 258=INVALID_ARG decode (correcting the upstream community timeout assumption) came from checking our actual esp_err.h. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
502 lines
12 KiB
C
502 lines
12 KiB
C
/*
|
|
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include "host_power_save.h"
|
|
#include "esp_hosted_transport_init.h"
|
|
#include "esp_hosted_interface.h"
|
|
#include "host_power_save.h"
|
|
#include "driver/gpio.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
#include "freertos/timers.h"
|
|
#include "esp_log.h"
|
|
#include <string.h>
|
|
#include "esp_timer.h"
|
|
#include "interface.h"
|
|
static char *TAG = "host_ps";
|
|
|
|
#if H_HOST_PS_ALLOWED
|
|
|
|
static host_power_save_config_t hps_config = HOST_POWER_SAVE_DEFAULT_CONFIG_DISABLED();
|
|
uint8_t power_save_on;
|
|
|
|
#if H_HOST_PS_DEEP_SLEEP_ALLOWED
|
|
SemaphoreHandle_t wakeup_sem;
|
|
|
|
/* Wakeup GPIO control macros */
|
|
#define set_host_wakeup_gpio() gpio_set_level(hps_config.host_wakeup_gpio, hps_config.host_wakeup_level)
|
|
#define reset_host_wakeup_gpio() gpio_set_level(hps_config.host_wakeup_gpio, !hps_config.host_wakeup_level)
|
|
#endif
|
|
|
|
static esp_err_t configure_host_wakeup_gpio(uint32_t gpio_num, uint8_t level)
|
|
{
|
|
esp_err_t ret = ESP_OK;
|
|
/* Make sure the GPIO is configured */
|
|
gpio_config_t io_conf = {
|
|
.intr_type = GPIO_INTR_DISABLE,
|
|
.mode = GPIO_MODE_OUTPUT,
|
|
.pin_bit_mask = (1ULL << gpio_num)
|
|
};
|
|
|
|
ret = gpio_config(&io_conf);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to configure host wakeup GPIO: %d", ret);
|
|
return ret;
|
|
}
|
|
|
|
/* Configure pull based on wakeup level */
|
|
if (level) {
|
|
ret = gpio_pulldown_en(gpio_num);
|
|
} else {
|
|
ret = gpio_pullup_en(gpio_num);
|
|
}
|
|
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to configure pull based on wakeup level: %d", ret);
|
|
return ret;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
extern interface_context_t *if_context;
|
|
extern interface_handle_t *if_handle;
|
|
|
|
int is_host_wakeup_needed(interface_buffer_handle_t *buf_handle)
|
|
{
|
|
int wakup_needed = 0;
|
|
char reason[100] = "";
|
|
#if H_HOST_PS_ALLOWED
|
|
uint8_t *buf_start;
|
|
|
|
buf_start = buf_handle->payload;
|
|
|
|
#if CONFIG_ESP_SPI_HD_HOST_INTERFACE || CONFIG_ESP_UART_HOST_INTERFACE || CONFIG_ESP_SPI_HOST_INTERFACE
|
|
/* Flow control packet cannot miss */
|
|
if (buf_handle->wifi_flow_ctrl_en) {
|
|
strlcpy(reason, "flow_ctl_pkt", sizeof(reason));
|
|
wakup_needed = 1;
|
|
goto end;
|
|
}
|
|
#endif
|
|
|
|
if (!buf_start) {
|
|
/* Do not wake up */
|
|
strlcpy(reason, "NULL_TxBuff", sizeof(reason));
|
|
wakup_needed = 0;
|
|
goto end;
|
|
}
|
|
|
|
/* Wake up for serial msg */
|
|
switch (buf_handle->if_type) {
|
|
|
|
case ESP_SERIAL_IF:
|
|
strlcpy(reason, "serial tx msg", sizeof(reason));
|
|
wakup_needed = 1;
|
|
goto end;
|
|
break;
|
|
|
|
case ESP_HCI_IF:
|
|
strlcpy(reason, "bt tx msg", sizeof(reason));
|
|
wakup_needed = 1;
|
|
goto end;
|
|
break;
|
|
|
|
case ESP_PRIV_IF:
|
|
strlcpy(reason, "priv tx msg", sizeof(reason));
|
|
wakup_needed = 1;
|
|
goto end;
|
|
break;
|
|
|
|
case ESP_TEST_IF:
|
|
strlcpy(reason, "test tx msg", sizeof(reason));
|
|
wakup_needed = 1;
|
|
goto end;
|
|
break;
|
|
|
|
case ESP_STA_IF:
|
|
|
|
/* User can parse the incoming Wi-Fi frame here, for any selective wake up, or drop.
|
|
* if network split configured, you can also amend function,
|
|
* nw_split_filter_and_route_packet() to process at slave or selective forward to host,
|
|
* by inspecting frame/packet
|
|
**/
|
|
strlcpy(reason, "sta tx msg", sizeof(reason));
|
|
wakup_needed = 1;
|
|
goto end;
|
|
break;
|
|
|
|
case ESP_AP_IF:
|
|
strlcpy(reason, "ap tx msg", sizeof(reason));
|
|
wakup_needed = 1;
|
|
goto end;
|
|
break;
|
|
}
|
|
|
|
end:
|
|
#else
|
|
strlcpy(reason, "host_ps_disabled", sizeof(reason));
|
|
wakup_needed = 0;
|
|
#endif
|
|
|
|
if (wakup_needed) {
|
|
ESP_LOGI(TAG, "Wakeup needed, reason %s", reason);
|
|
} else {
|
|
ESP_LOGI(TAG, "Wakeup not needed");
|
|
}
|
|
return wakup_needed;
|
|
}
|
|
|
|
int host_power_save_init(host_power_save_config_t *config)
|
|
{
|
|
esp_err_t ret = ESP_OK;
|
|
#if H_HOST_PS_ALLOWED
|
|
if (config) {
|
|
memcpy(&hps_config, config, sizeof(host_power_save_config_t));
|
|
} else {
|
|
hps_config = (host_power_save_config_t){
|
|
.enable = 1,
|
|
.host_wakeup_gpio = H_HOST_WAKE_UP_GPIO,
|
|
.host_wakeup_level = H_HOST_WAKEUP_GPIO_LEVEL,
|
|
.callbacks = {0}
|
|
};
|
|
}
|
|
|
|
if (!hps_config.enable) {
|
|
ESP_LOGI(TAG, "Host power save disabled via config");
|
|
return ret;
|
|
}
|
|
|
|
#if H_HOST_PS_DEEP_SLEEP_ALLOWED
|
|
/* Configure GPIO from config or use Kconfig default */
|
|
assert(hps_config.host_wakeup_gpio != -1);
|
|
|
|
/* Configure the host wakeup GPIO */
|
|
ret = configure_host_wakeup_gpio(hps_config.host_wakeup_gpio, hps_config.host_wakeup_level);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to configure host wakeup GPIO: %d", ret);
|
|
return ret;
|
|
}
|
|
ESP_LOGI(TAG, "Host wakeup: IO%u, level:%u (configured)",
|
|
hps_config.host_wakeup_gpio, gpio_get_level(hps_config.host_wakeup_gpio));
|
|
reset_host_wakeup_gpio();
|
|
|
|
ESP_LOGI(TAG, "Host wakeup: IO%u, level:%u (active %s)",
|
|
hps_config.host_wakeup_gpio, gpio_get_level(hps_config.host_wakeup_gpio),
|
|
hps_config.host_wakeup_level ? "HIGH" : "LOW");
|
|
|
|
if (!wakeup_sem) {
|
|
assert(wakeup_sem = xSemaphoreCreateBinary());
|
|
xSemaphoreGive(wakeup_sem);
|
|
}
|
|
#endif
|
|
|
|
/* Store callbacks from config */
|
|
if (hps_config.callbacks.host_power_save_on_prepare_cb ||
|
|
hps_config.callbacks.host_power_save_on_ready_cb ||
|
|
hps_config.callbacks.host_power_save_off_prepare_cb ||
|
|
hps_config.callbacks.host_power_save_off_ready_cb) {
|
|
ESP_LOGI(TAG, "Host power save callbacks registered");
|
|
} else {
|
|
ESP_LOGI(TAG, "Host power save init without callbacks (manual control)");
|
|
}
|
|
#endif
|
|
return ret;
|
|
}
|
|
|
|
int host_power_save_deinit(void)
|
|
{
|
|
#if H_HOST_PS_ALLOWED
|
|
#if H_HOST_PS_DEEP_SLEEP_ALLOWED
|
|
if (wakeup_sem) {
|
|
xSemaphoreTake(wakeup_sem, portMAX_DELAY);
|
|
xSemaphoreGive(wakeup_sem);
|
|
vSemaphoreDelete(wakeup_sem);
|
|
wakeup_sem = NULL;
|
|
}
|
|
#endif
|
|
/* Clear callbacks */
|
|
memset(&hps_config.callbacks, 0, sizeof(hps_config.callbacks));
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
int host_power_save_set_callbacks(host_power_save_callbacks_t *new_callbacks)
|
|
{
|
|
#if H_HOST_PS_ALLOWED
|
|
if (!new_callbacks) {
|
|
ESP_LOGE(TAG, "NULL callbacks provided");
|
|
return -1;
|
|
}
|
|
|
|
/* Warn if replacing existing callbacks */
|
|
if (hps_config.callbacks.host_power_save_on_prepare_cb ||
|
|
hps_config.callbacks.host_power_save_on_ready_cb ||
|
|
hps_config.callbacks.host_power_save_off_prepare_cb ||
|
|
hps_config.callbacks.host_power_save_off_ready_cb) {
|
|
ESP_LOGW(TAG, "Replacing existing host power save callbacks");
|
|
}
|
|
|
|
/* Replace callbacks */
|
|
memcpy(&hps_config.callbacks, new_callbacks, sizeof(hps_config.callbacks));
|
|
ESP_LOGI(TAG, "Host power save callbacks updated");
|
|
|
|
return 0;
|
|
#else
|
|
ESP_LOGE(TAG, "Host power save not enabled");
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
#define GET_CURR_TIME_IN_MS() esp_timer_get_time()/100
|
|
|
|
/* Add new callback function for ESP Timer */
|
|
#if H_HOST_PS_ALLOWED && H_HOST_PS_DEEP_SLEEP_ALLOWED
|
|
static void clean_wakeup_gpio_timer_cb(void* arg)
|
|
{
|
|
reset_host_wakeup_gpio();
|
|
ESP_EARLY_LOGI(TAG, "Cleared wakeup gpio, IO%u", hps_config.host_wakeup_gpio);
|
|
}
|
|
#endif
|
|
|
|
#if H_HOST_PS_ALLOWED && H_HOST_PS_DEEP_SLEEP_ALLOWED
|
|
static int trigger_host_wakeup(uint32_t timeout_ms)
|
|
{
|
|
esp_timer_handle_t timer = NULL;
|
|
esp_err_t ret = ESP_OK;
|
|
uint64_t start_time = GET_CURR_TIME_IN_MS();
|
|
uint8_t wakeup_success = 0;
|
|
esp_timer_create_args_t timer_args = {
|
|
.callback = &clean_wakeup_gpio_timer_cb,
|
|
.name = "host_wakeup_timer",
|
|
};
|
|
|
|
ESP_LOGI(TAG, "WAKE UP Host!!!!!\n");
|
|
|
|
do {
|
|
set_host_wakeup_gpio();
|
|
|
|
/* Create ESP Timer instead of FreeRTOS timer */
|
|
if (!timer) {
|
|
ret = esp_timer_create(&timer_args, &timer);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to create timer for host wakeup");
|
|
break;
|
|
}
|
|
}
|
|
assert(timer);
|
|
|
|
/* Start one-shot timer (10ms) */
|
|
ret = esp_timer_start_once(timer, 10000); /* 10ms in microseconds */
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to start timer for host wakeup");
|
|
esp_timer_delete(timer);
|
|
timer = NULL;
|
|
break;
|
|
}
|
|
vTaskDelay(100);
|
|
|
|
if (wakeup_sem) {
|
|
/* wait for host resume */
|
|
ret = xSemaphoreTake(wakeup_sem, pdMS_TO_TICKS(100));
|
|
if (ret == pdPASS) {
|
|
ESP_LOGI(TAG, "Wakeup semaphore acquired - host responded");
|
|
xSemaphoreGive(wakeup_sem);
|
|
wakeup_success = 1;
|
|
break;
|
|
} else {
|
|
ESP_LOGD(TAG, "Wakeup semaphore wait timeout, retrying...");
|
|
}
|
|
}
|
|
|
|
if (GET_CURR_TIME_IN_MS() - start_time > timeout_ms) {
|
|
/* timeout */
|
|
ESP_LOGI(TAG, "%s:%u timeout Curr:%llu start:%llu timeout:%lu",
|
|
__func__,__LINE__, GET_CURR_TIME_IN_MS(), start_time, timeout_ms);
|
|
break;
|
|
}
|
|
|
|
} while (1);
|
|
|
|
/* Clean up timer if it's still active */
|
|
if (timer) {
|
|
esp_timer_stop(timer);
|
|
esp_timer_delete(timer);
|
|
timer = NULL;
|
|
}
|
|
|
|
return wakeup_success;
|
|
}
|
|
#endif
|
|
|
|
int wakeup_host_mandate(uint32_t timeout_ms)
|
|
{
|
|
#if H_HOST_PS_ALLOWED && H_HOST_PS_DEEP_SLEEP_ALLOWED
|
|
esp_err_t ret = ESP_OK;
|
|
ESP_LOGI(TAG, "Mandate host wakeup");
|
|
|
|
/* Configure the host wakeup GPIO */
|
|
ret = configure_host_wakeup_gpio(hps_config.host_wakeup_gpio, hps_config.host_wakeup_level);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to configure host wakeup GPIO: %d", ret);
|
|
return ret;
|
|
}
|
|
|
|
/* Trigger host wakeup */
|
|
return trigger_host_wakeup(timeout_ms);
|
|
#else
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
int wakeup_host(uint32_t timeout_ms)
|
|
{
|
|
|
|
#if H_HOST_PS_ALLOWED
|
|
int wakeup_success = 0;
|
|
|
|
if(!is_host_power_saving()) {
|
|
return 1;
|
|
}
|
|
|
|
if (!hps_config.enable) {
|
|
ESP_LOGW(TAG, "%s: host_power_save_init never called, ignore", __func__);
|
|
return 1;
|
|
}
|
|
|
|
if (!if_handle || !if_context) {
|
|
ESP_LOGE(TAG, "Failed to wakeup, if_handle or if_context is NULL");
|
|
return 0;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "if_handle->state: %u", if_handle->state);
|
|
if (if_handle->state < DEACTIVE) {
|
|
ESP_LOGI(TAG, "%s:%u Re-Initializing driver\n", __func__, __LINE__);
|
|
|
|
/* host wakeup mandated in sdio init */
|
|
wakeup_success = 1;
|
|
if_handle = if_context->if_ops->init();
|
|
if (!if_handle) {
|
|
ESP_LOGE(TAG, "%s:%u Failed to initialize driver\n", __func__, __LINE__);
|
|
return ESP_FAIL;
|
|
}
|
|
}
|
|
|
|
if (is_host_power_saving()) {
|
|
wakeup_success = trigger_host_wakeup(timeout_ms);
|
|
ESP_LOGI(TAG, "host %s woke up", is_host_power_saving() ? "not" : "");
|
|
}
|
|
|
|
return wakeup_success;
|
|
#else
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
int host_power_save_alert(uint32_t ps_evt)
|
|
{
|
|
|
|
#if H_HOST_PS_ALLOWED
|
|
/* Running in interrupt context - Keep it short and simple */
|
|
BaseType_t do_yeild = pdFALSE;
|
|
|
|
if (!hps_config.enable) {
|
|
ESP_EARLY_LOGW(TAG, "%s: host_power_save_init never called, ignore", __func__);
|
|
return 0;
|
|
}
|
|
|
|
if (ESP_POWER_SAVE_ON == ps_evt) {
|
|
ESP_EARLY_LOGI(TAG, "Host Sleep");
|
|
|
|
/* USER CALLBACK: Prepare to enter power save */
|
|
if (hps_config.callbacks.host_power_save_on_prepare_cb) {
|
|
hps_config.callbacks.host_power_save_on_prepare_cb();
|
|
}
|
|
|
|
#if H_HOST_PS_DEEP_SLEEP_ALLOWED
|
|
if (wakeup_sem) {
|
|
/* Host sleeping */
|
|
/* Check if we're in ISR context */
|
|
if (xPortInIsrContext()) {
|
|
xSemaphoreTakeFromISR(wakeup_sem, &do_yeild);
|
|
} else {
|
|
/* Task context - use regular take with no timeout (should succeed immediately) */
|
|
xSemaphoreTake(wakeup_sem, 0);
|
|
}
|
|
}
|
|
#endif
|
|
power_save_on = 1;
|
|
|
|
if (!if_handle || !if_context || if_handle->state < DEACTIVE) {
|
|
ESP_EARLY_LOGE(TAG, "%s:%u Failed to bring down transport", __func__, __LINE__);
|
|
}
|
|
|
|
if (if_handle->state >= DEACTIVE) {
|
|
if (!if_context->if_ops || !if_context->if_ops->deinit) {
|
|
ESP_EARLY_LOGI(TAG, "%s:%u if_context->if_ops->deinit not available", __func__, __LINE__);
|
|
} else {
|
|
ESP_EARLY_LOGI(TAG, "%s:%u Deinitializing driver", __func__, __LINE__);
|
|
if_context->if_ops->deinit(if_handle);
|
|
/* if_handle->state would be changed to DEINIT */
|
|
}
|
|
}
|
|
|
|
/* USER CALLBACK: Power save active, device ready */
|
|
if (hps_config.callbacks.host_power_save_on_ready_cb) {
|
|
hps_config.callbacks.host_power_save_on_ready_cb();
|
|
}
|
|
|
|
} else if ((ESP_POWER_SAVE_OFF == ps_evt) || (ESP_OPEN_DATA_PATH == ps_evt)) {
|
|
ESP_EARLY_LOGI(TAG, "Host Awake, transport state: %u", if_handle->state);
|
|
|
|
/* USER CALLBACK: Prepare to exit power save */
|
|
if (hps_config.callbacks.host_power_save_off_prepare_cb) {
|
|
hps_config.callbacks.host_power_save_off_prepare_cb();
|
|
}
|
|
|
|
power_save_on = 0;
|
|
|
|
#if H_HOST_PS_DEEP_SLEEP_ALLOWED
|
|
if (wakeup_sem) {
|
|
ESP_EARLY_LOGI(TAG, "Giving wakeup semaphore");
|
|
/* Check if we're in ISR context */
|
|
if (xPortInIsrContext()) {
|
|
xSemaphoreGiveFromISR(wakeup_sem, &do_yeild);
|
|
} else {
|
|
/* Task context - use regular give */
|
|
xSemaphoreGive(wakeup_sem);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* USER CALLBACK: Power save off, device ready */
|
|
if (hps_config.callbacks.host_power_save_off_ready_cb) {
|
|
hps_config.callbacks.host_power_save_off_ready_cb();
|
|
}
|
|
|
|
} else {
|
|
ESP_EARLY_LOGI(TAG, "Ignore event[%u]", ps_evt);
|
|
}
|
|
#if 0
|
|
/* Only yield from ISR if we're actually in ISR context */
|
|
if (do_yeild == pdTRUE && xPortInIsrContext()) {
|
|
portYIELD_FROM_ISR();
|
|
}
|
|
#endif
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
int is_host_power_saving(void)
|
|
{
|
|
#if H_HOST_PS_ALLOWED
|
|
return power_save_on;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|