diff --git a/components/freertos/Kconfig b/components/freertos/Kconfig index 7df72816bff..9eb7f7311fc 100644 --- a/components/freertos/Kconfig +++ b/components/freertos/Kconfig @@ -593,6 +593,30 @@ menu "FreeRTOS" (no direct calls, but also no Bluetooth/WiFi), you can try enable this to cause xTaskCreateStatic to allow tasks stack in external memory. + config FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM + bool "Place dynamic task stacks in PSRAM by default" + depends on FREERTOS_TASK_CREATE_ALLOW_EXT_MEM + default n + help + When enabled, task stacks allocated by xTaskCreate() / xTaskCreatePinnedToCore() are + placed in PSRAM (with internal RAM as fallback). TCBs are still allocated from internal RAM. + + Restrictions that still require user attention: + + - Flash operations cannot be called from tasks a PSRAM stack as they will + run while cache is disabled. This includes all filesystem access and NVS. + The recommended approach here is to use esp-flash-dispatcher, + + idf.py add-dependency "espressif/esp_flash_dispatcher" + + which automatically routes all flash API calls to a dedicated task + placed in internal RAM. + - Deep-sleep requests issued from a task with a PSRAM stack are rejected at runtime. Auto-light-sleep + is unaffected because it runs from the idle task, whose stack is always in internal RAM. + - Components that spawn tasks via xTaskCreate and require cache-disabled-safe stacks + should use xTaskCreateWithCaps(..., MALLOC_CAP_INTERNAL) instead. + - xTaskCreateStatic is unaffected; the caller always decides where the stack lives. + endmenu # Extra # Hidden or compatibility options diff --git a/components/freertos/config/include/freertos/FreeRTOSConfig.h b/components/freertos/config/include/freertos/FreeRTOSConfig.h index 4634a2dec08..993261ca052 100644 --- a/components/freertos/config/include/freertos/FreeRTOSConfig.h +++ b/components/freertos/config/include/freertos/FreeRTOSConfig.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 */ @@ -136,7 +136,11 @@ #define configSUPPORT_STATIC_ALLOCATION 1 #define configSUPPORT_DYNAMIC_ALLOCATION 1 #define configAPPLICATION_ALLOCATED_HEAP 1 -#define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP 0 +#if CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM + #define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP 1 +#else + #define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP 0 +#endif /* ------------------------ Hooks -------------------------- */ diff --git a/components/freertos/heap_idf.c b/components/freertos/heap_idf.c index 60974047097..c9afb2bffc8 100644 --- a/components/freertos/heap_idf.c +++ b/components/freertos/heap_idf.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -94,6 +94,18 @@ bool xPortCheckValidTCBMem(const void * ptr) #endif /* CONFIG_IDF_TARGET_LINUX */ } +#if CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM +void * pvPortMallocStack(size_t xWantedSize) +{ + return heap_caps_malloc_prefer(xWantedSize, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); +} + +void vPortFreeStack(void * pv) +{ + heap_caps_free(pv); +} +#endif /* CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM */ + bool xPortcheckValidStackMem(const void * ptr) { #if CONFIG_IDF_TARGET_LINUX diff --git a/components/spi_flash/hints.yml b/components/spi_flash/hints.yml new file mode 100644 index 00000000000..dc784ce63f0 --- /dev/null +++ b/components/spi_flash/hints.yml @@ -0,0 +1,3 @@ +- + re: "assert failed: .*?\\(s_task_stack_is_sane_when_cache_frozen\\(\\)\\)" + hint: "Flash operations cannot be performed when task stack is in PSRAM. Check out esp_flash_dispatcher component at https://components.espressif.com/components/espressif/esp_flash_dispatcher/versions/1.0.1/readme?language= for a potential solution to this issue." diff --git a/docs/en/api-guides/external-ram.rst b/docs/en/api-guides/external-ram.rst index 358a08ab0a6..eaa76ecd6e8 100644 --- a/docs/en/api-guides/external-ram.rst +++ b/docs/en/api-guides/external-ram.rst @@ -221,10 +221,30 @@ External RAM use has the following restrictions: - External RAM uses the same cache region as the external flash. This means that frequently accessed variables in external RAM can be read and modified almost as quickly as in internal RAM. However, when accessing large chunks of data (> 32 KB), the cache can be insufficient, and speeds will fall back to the access speed of the external RAM. Moreover, accessing large chunks of data can "push out" cached flash, possibly making the execution of code slower afterwards. - - In general, external RAM will not be used as task stack memory. :cpp:func:`xTaskCreate` and similar functions will always allocate internal memory for stack and task TCBs. + - In general, external RAM will not be used as task stack memory. :cpp:func:`xTaskCreate` and similar functions will always allocate internal memory for stack and task TCBs. Task stacks can optionally be placed in external RAM — see :ref:`task-stack-in-external-ram` below. -The option :ref:`CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM` can be used to allow placing task stacks into external memory. In these cases :cpp:func:`xTaskCreateStatic` must be used to specify a task stack buffer allocated from external memory, otherwise task stacks will still be allocated from internal memory. +.. _task-stack-in-external-ram: +Task Stack Placement in External RAM +------------------------------------- + +There are three ways to place task stacks in external RAM: + +1. **Per-task (explicit)** – Use :cpp:func:`xTaskCreateWithCaps` with ``MALLOC_CAP_SPIRAM``. Requires :ref:`CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM`. + +2. **Per-task (static)** – Use :cpp:func:`xTaskCreateStatic` with a caller-supplied buffer in external RAM. Requires :ref:`CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM`. + +3. **Global default (automatic)** – Enable :ref:`CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM`. When set, every call to :cpp:func:`xTaskCreate` / :cpp:func:`xTaskCreatePinnedToCore` allocates the stack from PSRAM first, falling back to internal RAM if PSRAM is exhausted. TCBs are always kept in internal DRAM. + + +When :ref:`CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM` is enabled, the following additional restrictions apply: + +- **Flash operations** – Any code path that temporarily disables the CPU cache (flash erase/write, NVS, OTA) must run on a task whose stack is in internal RAM, or the operations must be routed through the `espressif/esp_flash_dispatcher `__ component, which executes flash operations on a dedicated internal-RAM task. +- **Deep sleep** – Calling :cpp:func:`esp_deep_sleep_start` from a task with a PSRAM stack logs an error and proceeds anyway, which will likely crash when the cache is disabled during the sleep transition. Use :cpp:func:`esp_deep_sleep_try_to_start` instead: it returns :c:macro:`ESP_ERR_NOT_ALLOWED` cleanly when called from a PSRAM-stacked task. If deep sleep is required, trigger it from a task whose stack is in internal RAM. +- **Light sleep** – Calling :cpp:func:`esp_light_sleep_start` directly from a PSRAM-stacked task is also rejected with :c:macro:`ESP_ERR_NOT_ALLOWED`. Tickless-idle light sleep (auto-light-sleep) is safe because it is initiated by the idle task, whose stack is always in internal RAM; PSRAM-stacked tasks simply block and resume normally after wake-up. +- **pthread** – :cpp:func:`pthread_create` delegates to :cpp:func:`xTaskCreate`, so pthread stacks will also move to PSRAM when this option is on. + +See the :example:`system/freertos/psram_stack` example for a working demonstration. Failure to Initialize ===================== diff --git a/docs/en/api-guides/performance/ram-usage.rst b/docs/en/api-guides/performance/ram-usage.rst index 142ccdf8649..9457d6b57d1 100644 --- a/docs/en/api-guides/performance/ram-usage.rst +++ b/docs/en/api-guides/performance/ram-usage.rst @@ -102,6 +102,10 @@ Reducing Stack Sizes - Avoid allocating large variables on the stack. In C, any large structures or arrays allocated as an automatic variable (i.e., default scope of a C declaration) uses space on the stack. To minimize the sizes of these, allocate them statically and/or see if you can save memory by dynamically allocating them from the heap only when they are needed. - Avoid deep recursive function calls. Individual recursive function calls do not always add a lot of stack usage each time they are called, but if each function includes large stack-based variables then the overhead can get quite high. +.. only:: SOC_SPIRAM_SUPPORTED + + If the application uses external RAM, enabling :ref:`CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM` can move task stacks created by :cpp:func:`xTaskCreate` or :cpp:func:`xTaskCreatePinnedToCore` to PSRAM, reducing internal RAM usage. This option has restrictions for tasks that run while the flash cache is disabled; see :ref:`task-stack-in-external-ram` for details. + Reducing Task Count ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/zh_CN/api-guides/external-ram.rst b/docs/zh_CN/api-guides/external-ram.rst index 71e7e5e39c8..25d4342caed 100644 --- a/docs/zh_CN/api-guides/external-ram.rst +++ b/docs/zh_CN/api-guides/external-ram.rst @@ -221,9 +221,29 @@ ESP-IDF 启动过程中,片外 RAM 被映射到数据虚拟地址空间,该 - 片外 RAM 与片外 flash 使用相同的 cache 区域,这意味着频繁在片外 RAM 访问的变量可以像在片上 RAM 中一样快速读取和修改。但访问大块数据时(大于 32 KB),cache 空间可能会不足,访问速度将降低到片外 RAM 的访问速度。此外,访问大块数据会挤出 flash cache,可能在之后降低代码的执行速度。 - - 一般来说,片外 RAM 不会用作任务堆栈存储器。:cpp:func:`xTaskCreate` 及类似函数始终会为堆栈和任务 TCB 分配片上储存器。 + - 一般来说,片外 RAM 不会用作任务堆栈存储器。:cpp:func:`xTaskCreate` 及类似函数始终会为堆栈和任务 TCB 分配片上储存器。任务堆栈也可选择放入片外 RAM——详见下方 :ref:`task-stack-in-external-ram`。 -可以使用 :ref:`CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM` 选项将任务堆栈放入片外存储器。这时,必须使用 :cpp:func:`xTaskCreateStatic` 指定从片外存储器分配的任务堆栈缓冲区,否则任务堆栈将仍从片上存储器分配。 +.. _task-stack-in-external-ram: + +将任务堆栈放入片外存储器 +------------------------------- + +有三种方式可将任务堆栈放入片外 RAM: + +1. **单任务(显式)** – 使用 ``MALLOC_CAP_SPIRAM`` 标志调用 :cpp:func:`xTaskCreateWithCaps`。需要启用 :ref:`CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM`。 + +2. **单任务(静态)** – 使用 :cpp:func:`xTaskCreateStatic`,提供位于片外 RAM 的调用方自定义缓冲区。需要启用 :ref:`CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM`。 + +3. **全局默认(自动)** – 启用 :ref:`CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM`。启用后,:cpp:func:`xTaskCreate` / :cpp:func:`xTaskCreatePinnedToCore` 的每次调用都会优先从 PSRAM 分配任务堆栈,若 PSRAM 耗尽则回退到片上 RAM。TCB 始终保留在片上 DRAM 中。 + +启用 :ref:`CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM` 后,还需注意以下额外限制: + +- **Flash 操作** – 任何会暂时禁用 CPU cache 的代码路径(flash 擦除/写入、NVS、OTA)必须在堆栈位于片上 RAM 的任务中运行,或通过 `espressif/esp_flash_dispatcher `__ 组件路由,该组件会在专用片上 RAM 任务中执行 flash 操作。 +- **深度睡眠** – 从堆栈位于 PSRAM 的任务中调用 :cpp:func:`esp_deep_sleep_start` 会记录错误日志并继续执行,但在睡眠过程中禁用 cache 时极有可能发生崩溃。建议改用 :cpp:func:`esp_deep_sleep_try_to_start`:当从 PSRAM 堆栈任务中调用时,该函数会返回 :c:macro:`ESP_ERR_NOT_ALLOWED` 而不会崩溃。若应用程序需要深度睡眠,请从堆栈位于片上 RAM 的任务中发起调用。 +- **浅睡眠** – 从堆栈位于 PSRAM 的任务中直接调用 :cpp:func:`esp_light_sleep_start` 同样会返回 :c:macro:`ESP_ERR_NOT_ALLOWED`。Tickless idle 自动浅睡眠是安全的,因为它由 idle 任务发起,而 idle 任务的堆栈始终位于片上 RAM;堆栈在 PSRAM 中的任务在唤醒后可正常恢复执行。 +- **pthread** – :cpp:func:`pthread_create` 委托给 :cpp:func:`xTaskCreate`,因此启用此选项后,pthread 堆栈也会移至 PSRAM。 + +相关演示请参考 :example:`system/freertos/psram_stack` 示例。 初始化失败 diff --git a/docs/zh_CN/api-guides/performance/ram-usage.rst b/docs/zh_CN/api-guides/performance/ram-usage.rst index 3fa620de94c..cd5f1c19dd8 100644 --- a/docs/zh_CN/api-guides/performance/ram-usage.rst +++ b/docs/zh_CN/api-guides/performance/ram-usage.rst @@ -102,6 +102,10 @@ ESP-IDF 包含一系列堆 API,可以在运行时测量空闲堆内存,请 - 避免在栈上分配大型变量。在 C 语言声明的默认作用域中,任何分配为自动变量的大型结构体或数组都会占用栈内存。要优化这些变量占用的栈内存大小,可以使用静态分配,或仅在需要时从堆中动态分配。 - 避免调用深度递归函数。尽管调用单个递归函数并不一定会占用大量栈内存,但若每个函数都包含大量基于栈的变量,那么调用这些函数的开销将会很高。 +.. only:: SOC_SPIRAM_SUPPORTED + + 如果应用程序使用外部 RAM,启用 :ref:`CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM` 可以将 :cpp:func:`xTaskCreate` 或 :cpp:func:`xTaskCreatePinnedToCore` 创建的任务栈移至 PSRAM,从而减少内部 RAM 使用量。此选项对在 flash cache 禁用期间运行的任务有限制,详情请参阅 :ref:`task-stack-in-external-ram`。 + 减少任务数量 ^^^^^^^^^^^^ diff --git a/examples/system/freertos/.build-test-rules.yml b/examples/system/freertos/.build-test-rules.yml index 69d3b1b57c9..40b42f396ab 100644 --- a/examples/system/freertos/.build-test-rules.yml +++ b/examples/system/freertos/.build-test-rules.yml @@ -6,6 +6,13 @@ examples/system/freertos/basic_freertos_smp_usage: - *common_components - freertos +examples/system/freertos/psram_stack: + disable: + - if: SOC_SPIRAM_SUPPORTED != 1 + reason: test requires PSRAM, and these are the targets with PSRAM support + depends_components: + - freertos + examples/system/freertos/real_time_stats: disable: - if: IDF_TARGET != "esp32" and (NIGHTLY_RUN != "1" or IDF_TARGET == "linux") diff --git a/examples/system/freertos/psram_stack/CMakeLists.txt b/examples/system/freertos/psram_stack/CMakeLists.txt new file mode 100644 index 00000000000..afd96b6d98d --- /dev/null +++ b/examples/system/freertos/psram_stack/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) + +project(psram_stack) diff --git a/examples/system/freertos/psram_stack/README.md b/examples/system/freertos/psram_stack/README.md new file mode 100644 index 00000000000..2bd6d28ec11 --- /dev/null +++ b/examples/system/freertos/psram_stack/README.md @@ -0,0 +1,56 @@ +| Supported Targets | ESP32 | ESP32-C5 | ESP32-C61 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 | +| ----------------- | ----- | -------- | --------- | -------- | -------- | -------- | -------- | --------- | + +# FreeRTOS PSRAM Stack Example + +(See the README.md file in the upper level `examples` directory for more information about examples.) + +This example demonstrates `CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM`, which causes `xTaskCreate()` and `xTaskCreatePinnedToCore()` to allocate task stacks from PSRAM by default (with internal RAM as a fallback). TCBs are always kept in internal DRAM. + +It also shows how to use the `esp_flash_dispatcher` component so that flash operations called from PSRAM-stacked tasks remain safe. + +Any code path that disables the cache (flash erase/write, OTA, NVS) must run on a task whose stack is in internal RAM, or must go through `esp_flash_dispatcher`. + +## What is being demonstrated + +1. Three worker tasks are created with `xTaskCreate()`. Each task prints the address of a local variable to confirm its stack resides in PSRAM. +2. Each worker calls `esp_partition_read()` from its PSRAM stack. The `esp_flash_dispatcher` component intercepts the call and executes it on a dedicated internal-RAM task, avoiding a crash when the cache is temporarily disabled. + +## How to use example + +### Hardware Required + +A board with a PSRAM-capable SoC: ESP32-S2, ESP32-S3, or ESP32-P4. + +### Configure the project + +``` +idf.py menuconfig +``` + +The `sdkconfig.defaults` already enables `CONFIG_SPIRAM`, `CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM`, and `CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM`. No additional configuration is required. + +### Build and Flash + +``` +idf.py build flash monitor +``` + +### Example output + +``` +I (xxx) psram_stack: Initialising flash dispatcher (routes flash ops off PSRAM-stacked tasks) +I (xxx) psram_stack: worker[0]: stack @ 0x3c0xxxxx -> PSRAM +I (xxx) psram_stack: worker[0]: flash read OK (first bytes: e9 04 ...) +I (xxx) psram_stack: worker[1]: stack @ 0x3c0xxxxx -> PSRAM +I (xxx) psram_stack: worker[1]: flash read OK (first bytes: e9 04 ...) +I (xxx) psram_stack: worker[2]: stack @ 0x3c0xxxxx -> PSRAM +I (xxx) psram_stack: worker[2]: flash read OK (first bytes: e9 04 ...) +I (xxx) psram_stack: Example complete +``` + +## Further reading + +- `CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM` in `menuconfig → Component config → FreeRTOS → Extra` +- [External RAM](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-guides/external-ram.html) — limitations of tasks with PSRAM stacks +- `espressif/esp_flash_dispatcher` on the [ESP Component Registry](https://components.espressif.com/components/espressif/esp_flash_dispatcher) diff --git a/examples/system/freertos/psram_stack/main/CMakeLists.txt b/examples/system/freertos/psram_stack/main/CMakeLists.txt new file mode 100644 index 00000000000..80db9528552 --- /dev/null +++ b/examples/system/freertos/psram_stack/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "psram_stack_example_main.c" + PRIV_REQUIRES esp_psram esp_partition + INCLUDE_DIRS ".") diff --git a/examples/system/freertos/psram_stack/main/idf_component.yml b/examples/system/freertos/psram_stack/main/idf_component.yml new file mode 100644 index 00000000000..6c60f71acfa --- /dev/null +++ b/examples/system/freertos/psram_stack/main/idf_component.yml @@ -0,0 +1,3 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/esp_flash_dispatcher: ==1.0.1 diff --git a/examples/system/freertos/psram_stack/main/psram_stack_example_main.c b/examples/system/freertos/psram_stack/main/psram_stack_example_main.c new file mode 100644 index 00000000000..31edeab2fd5 --- /dev/null +++ b/examples/system/freertos/psram_stack/main/psram_stack_example_main.c @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file psram_stack_example_main.c + * @brief Demonstrates CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM. + * + * When CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM=y, xTaskCreate() places + * task stacks in PSRAM automatically. TCBs always stay in internal DRAM. + * + * Flash operations (e.g. esp_partition_read) are safe from these tasks + * because the esp_flash_dispatcher component intercepts flash APIs and runs + * the actual operation on a dedicated internal-RAM task. + */ + +#include +#include +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_heap_caps.h" +#include "esp_memory_utils.h" +#include "esp_partition.h" +#include "esp_log.h" +#include "esp_flash_dispatcher.h" + +#define NUM_WORKER_TASKS 3 +#define WORKER_STACK_SIZE 4096 +#define WORKER_PRIORITY 5 + +static const char *TAG = "psram_stack"; + +typedef struct { + int task_idx; + TaskHandle_t notify_handle; +} worker_args_t; + +static void worker_task(void *pvArg) +{ + worker_args_t *args = (worker_args_t *)pvArg; + + /* Probe a local variable to confirm this stack is in PSRAM. */ + volatile uint8_t stack_probe = 0; + void *stack_ptr = (void *)&stack_probe; + + ESP_LOGI(TAG, "worker[%d]: stack @ %p -> PSRAM", args->task_idx, stack_ptr); + assert(esp_ptr_external_ram(stack_ptr)); + + + /* Perform a flash read from a PSRAM-stacked task. + * esp_flash_dispatcher intercepts both spi_flash_mmap (used by + * esp_partition_find_first on its first call) and esp_flash_read + * (used by esp_partition_read), executing them on the internal-RAM + * dispatcher task where they can safely run even when cache gets + * disabled during the flash operation. */ + const esp_partition_t *part = esp_partition_find_first( + ESP_PARTITION_TYPE_APP, + ESP_PARTITION_SUBTYPE_ANY, NULL); + if (part) { + uint8_t buf[16]; + esp_err_t err = esp_partition_read(part, 0, buf, sizeof(buf)); + if (err == ESP_OK) { + ESP_LOGI(TAG, "worker[%d]: flash read OK (first bytes: %02x %02x %02x %02x ...)", + args->task_idx, buf[0], buf[1], buf[2], buf[3]); + } else { + ESP_LOGE(TAG, "worker[%d]: flash read failed: %s", args->task_idx, esp_err_to_name(err)); + } + } + + xTaskNotifyGive(args->notify_handle); + vTaskDelete(NULL); +} + +void app_main(void) +{ + ESP_LOGI(TAG, "Initialising flash dispatcher (routes flash ops off PSRAM-stacked tasks)"); + const esp_flash_dispatcher_config_t flash_disp_cfg = { + .task_stack_size = 2048, + .task_priority = configMAX_PRIORITIES - 1, + .task_core_id = tskNO_AFFINITY, + .queue_size = 5, + }; + ESP_ERROR_CHECK(esp_flash_dispatcher_init(&flash_disp_cfg)); + + static worker_args_t args[NUM_WORKER_TASKS]; + TaskHandle_t self = xTaskGetCurrentTaskHandle(); + + for (int i = 0; i < NUM_WORKER_TASKS; i++) { + args[i].task_idx = i; + args[i].notify_handle = self; + xTaskCreate(worker_task, "worker", WORKER_STACK_SIZE, &args[i], + WORKER_PRIORITY, NULL); + } + + /* Wait for all workers to finish. */ + for (int i = 0; i < NUM_WORKER_TASKS; i++) { + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + } + + ESP_LOGI(TAG, "Example complete"); +} diff --git a/examples/system/freertos/psram_stack/pytest_psram_stack_example.py b/examples/system/freertos/psram_stack/pytest_psram_stack_example.py new file mode 100644 index 00000000000..ce0dd1c6a7d --- /dev/null +++ b/examples/system/freertos/psram_stack/pytest_psram_stack_example.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 +import re + +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize +from pytest_embedded_idf.utils import soc_filtered_targets + + +@pytest.mark.generic +@idf_parametrize('target', soc_filtered_targets('SOC_SPIRAM_SUPPORTED == 1'), indirect=['target']) +def test_psram_stack_example(dut: Dut) -> None: + dut.expect_exact('Initialising flash dispatcher') + + output = dut.expect_exact('Example complete', timeout=15, return_what_before_match=True).decode( + 'utf-8', errors='ignore' + ) + for i in range(3): + assert re.search(rf'worker\[{i}\]: stack @ 0x[0-9a-f]+ +-> PSRAM', output) + assert re.search(rf'worker\[{i}\]: flash read OK', output) diff --git a/examples/system/freertos/psram_stack/sdkconfig.defaults b/examples/system/freertos/psram_stack/sdkconfig.defaults new file mode 100644 index 00000000000..f6636928a14 --- /dev/null +++ b/examples/system/freertos/psram_stack/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_SPIRAM=y +CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM=y +CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM=y diff --git a/tools/test_apps/system/.build-test-rules.yml b/tools/test_apps/system/.build-test-rules.yml index 2927d909d58..25d6139d76c 100644 --- a/tools/test_apps/system/.build-test-rules.yml +++ b/tools/test_apps/system/.build-test-rules.yml @@ -152,6 +152,11 @@ tools/test_apps/system/panic/panic_base: enable: - if: INCLUDE_DEFAULT == 1 or IDF_TARGET in ["esp32s31", "esp32h4"] +tools/test_apps/system/psram_stack: + disable: + - if: SOC_PSRAM_SUPPORTED != 1 + reason: PSRAM stack feature requires a target with PSRAM support + tools/test_apps/system/ram_loadable_app: disable: - if: IDF_TARGET == "esp32p4" diff --git a/tools/test_apps/system/psram_stack/CMakeLists.txt b/tools/test_apps/system/psram_stack/CMakeLists.txt new file mode 100644 index 00000000000..8794126a5e5 --- /dev/null +++ b/tools/test_apps/system/psram_stack/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +idf_build_set_property(MINIMAL_BUILD ON) +project(psram_stack) diff --git a/tools/test_apps/system/psram_stack/README.md b/tools/test_apps/system/psram_stack/README.md new file mode 100644 index 00000000000..691f9018dee --- /dev/null +++ b/tools/test_apps/system/psram_stack/README.md @@ -0,0 +1,2 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 | Linux | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- | ----- | diff --git a/tools/test_apps/system/psram_stack/main/CMakeLists.txt b/tools/test_apps/system/psram_stack/main/CMakeLists.txt new file mode 100644 index 00000000000..62d9cefaccc --- /dev/null +++ b/tools/test_apps/system/psram_stack/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "test_psram_stack.c" + INCLUDE_DIRS "." + PRIV_REQUIRES unity nvs_flash esp_partition esp_hw_support esp_psram esp_pm) diff --git a/tools/test_apps/system/psram_stack/main/idf_component.yml b/tools/test_apps/system/psram_stack/main/idf_component.yml new file mode 100644 index 00000000000..6c60f71acfa --- /dev/null +++ b/tools/test_apps/system/psram_stack/main/idf_component.yml @@ -0,0 +1,3 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/esp_flash_dispatcher: ==1.0.1 diff --git a/tools/test_apps/system/psram_stack/main/test_psram_stack.c b/tools/test_apps/system/psram_stack/main/test_psram_stack.c new file mode 100644 index 00000000000..7038a60693b --- /dev/null +++ b/tools/test_apps/system/psram_stack/main/test_psram_stack.c @@ -0,0 +1,210 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * System-level tests for CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM. + * + * Unity is run from a task created with xTaskCreate(), so every TEST_CASE + * body executes directly on a PSRAM-backed stack. No worker-task indirection + * is needed except for the concurrency test. + */ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "unity.h" +#include "esp_attr.h" +#include "esp_heap_caps.h" +#include "esp_memory_utils.h" +#include "esp_partition.h" +#include "esp_pm.h" +#include "esp_sleep.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "esp_flash_dispatcher.h" +#include "esp_private/esp_clk.h" +#include "sdkconfig.h" + +#define WORKER_STACK_SIZE 4096 +#define WORKER_PRIORITY 5 +#define UNITY_STACK_SIZE 8192 +#define NUM_CONCURRENT 5 +#define CONCURRENT_READS 10 +#define MHZ 1000000 + +static const char *TAG = "psram_stack_test"; +#if CONFIG_PM_ENABLE && CONFIG_FREERTOS_USE_TICKLESS_IDLE && CONFIG_PM_LIGHT_SLEEP_CALLBACKS +static volatile uint32_t s_light_sleep_exit_count; +static volatile int64_t s_last_light_sleep_us; +#endif + +/* Confirm the calling task's stack is in PSRAM before each test body runs. */ +#define ASSERT_STACK_IN_PSRAM() \ + do { volatile uint8_t _probe = 0; \ + TEST_ASSERT_TRUE_MESSAGE(esp_ptr_external_ram((void *)&_probe), \ + "test task stack is not in PSRAM"); } while (0) + + +TEST_CASE("PSRAM stack: flash read via dispatcher succeeds", "[psram_stack]") +{ + ASSERT_STACK_IN_PSRAM(); + const esp_partition_t *part = esp_partition_find_first( + ESP_PARTITION_TYPE_APP, + ESP_PARTITION_SUBTYPE_ANY, NULL); + TEST_ASSERT_NOT_NULL(part); + uint8_t buf[16]; + TEST_ASSERT_EQUAL(ESP_OK, esp_partition_read(part, 0, buf, sizeof(buf))); +} + +TEST_CASE("PSRAM stack: NVS read/write via dispatcher succeeds", "[psram_stack]") +{ + ASSERT_STACK_IN_PSRAM(); + nvs_handle_t handle; + TEST_ASSERT_EQUAL(ESP_OK, nvs_open("psram_test", NVS_READWRITE, &handle)); + TEST_ASSERT_EQUAL(ESP_OK, nvs_set_u32(handle, "key", 0xDEADBEEFUL)); + TEST_ASSERT_EQUAL(ESP_OK, nvs_commit(handle)); + uint32_t val = 0; + TEST_ASSERT_EQUAL(ESP_OK, nvs_get_u32(handle, "key", &val)); + TEST_ASSERT_EQUAL_HEX32(0xDEADBEEFUL, val); + nvs_close(handle); +} + +TEST_CASE("PSRAM stack: deep sleep rejected from PSRAM-stacked task", "[psram_stack]") +{ + ASSERT_STACK_IN_PSRAM(); + esp_sleep_enable_timer_wakeup(1000000ULL); + TEST_ASSERT_EQUAL(ESP_ERR_NOT_ALLOWED, esp_deep_sleep_try_to_start()); +} + +#if CONFIG_PM_ENABLE && CONFIG_FREERTOS_USE_TICKLESS_IDLE && CONFIG_PM_LIGHT_SLEEP_CALLBACKS + +static esp_err_t IRAM_ATTR light_sleep_exit_cb(int64_t slept_us, void *arg) +{ + (void)arg; + + if (slept_us > 0) { + s_last_light_sleep_us = slept_us; + s_light_sleep_exit_count++; + } + + return ESP_OK; +} + +TEST_CASE("PSRAM stack: task resumes correctly after tickless-idle light sleep", "[psram_stack][light_sleep]") +{ + ASSERT_STACK_IN_PSRAM(); + + s_light_sleep_exit_count = 0; + s_last_light_sleep_us = 0; + + esp_pm_sleep_cbs_register_config_t sleep_cbs = { + .exit_cb = light_sleep_exit_cb, + }; + TEST_ESP_OK(esp_pm_light_sleep_unregister_cbs(&sleep_cbs)); + TEST_ESP_OK(esp_pm_light_sleep_register_cbs(&sleep_cbs)); + + printf("Waiting for tickless-idle light sleep...\n"); + vTaskDelay(pdMS_TO_TICKS(100)); + + const uint32_t exit_count = s_light_sleep_exit_count; + const int64_t last_slept_us = s_last_light_sleep_us; + TEST_ESP_OK(esp_pm_light_sleep_unregister_cbs(&sleep_cbs)); + + ESP_LOGI(TAG, "Auto light sleep exit callbacks: %" PRIu32 ", last slept: %" PRId64 " us", + exit_count, last_slept_us); + TEST_ASSERT_GREATER_THAN_UINT32(0, exit_count); + TEST_ASSERT_GREATER_THAN_UINT32(0, last_slept_us); + + ASSERT_STACK_IN_PSRAM(); +} + +#endif /* CONFIG_PM_ENABLE && CONFIG_FREERTOS_USE_TICKLESS_IDLE && CONFIG_PM_LIGHT_SLEEP_CALLBACKS */ + +/* ---------- Test 6: Concurrent flash reads from multiple PSRAM-stacked tasks ---------- */ + +typedef struct { + esp_err_t err; + int reads_done; + TaskHandle_t parent; +} concurrent_result_t; + +static void task_concurrent_flash(void *arg) +{ + concurrent_result_t *r = arg; + const esp_partition_t *part = esp_partition_find_first( + ESP_PARTITION_TYPE_APP, + ESP_PARTITION_SUBTYPE_ANY, NULL); + if (!part) { + r->err = ESP_ERR_NOT_FOUND; + xTaskNotifyGive(r->parent); + vTaskDelete(NULL); + return; + } + r->err = ESP_OK; + for (int i = 0; i < CONCURRENT_READS; i++) { + uint8_t buf[16]; + r->err = esp_partition_read(part, 0, buf, sizeof(buf)); + if (r->err != ESP_OK) { + break; + } + r->reads_done++; + } + xTaskNotifyGive(r->parent); + vTaskDelete(NULL); +} + +TEST_CASE("PSRAM stack: concurrent flash reads from multiple PSRAM-stacked tasks", "[psram_stack]") +{ + ASSERT_STACK_IN_PSRAM(); + static concurrent_result_t results[NUM_CONCURRENT]; + TaskHandle_t self = xTaskGetCurrentTaskHandle(); + + for (int i = 0; i < NUM_CONCURRENT; i++) { + results[i] = (concurrent_result_t){ .parent = self }; + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(task_concurrent_flash, "cflash", + WORKER_STACK_SIZE, &results[i], WORKER_PRIORITY, NULL)); + } + for (int i = 0; i < NUM_CONCURRENT; i++) { + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + } + for (int i = 0; i < NUM_CONCURRENT; i++) { + ESP_LOGI(TAG, "worker[%d]: %d reads, err=%s", + i, results[i].reads_done, esp_err_to_name(results[i].err)); + TEST_ASSERT_EQUAL(ESP_OK, results[i].err); + TEST_ASSERT_EQUAL(CONCURRENT_READS, results[i].reads_done); + } +} + + +static void unity_task(void *arg) +{ + unity_run_menu(); + vTaskDelete(NULL); +} + +void app_main(void) +{ + const esp_flash_dispatcher_config_t disp_cfg = { + .task_stack_size = 4096, + .task_priority = configMAX_PRIORITIES - 1, + .task_core_id = tskNO_AFFINITY, + .queue_size = 8, + }; + ESP_ERROR_CHECK(esp_flash_dispatcher_init(&disp_cfg)); + + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK(err); + + /* Run Unity from a PSRAM-stacked task so all TEST_CASE bodies execute + * with a PSRAM stack without needing per-test worker task indirection. */ + xTaskCreate(unity_task, "unity", UNITY_STACK_SIZE, NULL, WORKER_PRIORITY, NULL); +} diff --git a/tools/test_apps/system/psram_stack/pytest_psram_stack.py b/tools/test_apps/system/psram_stack/pytest_psram_stack.py new file mode 100644 index 00000000000..d6b8e0f9a93 --- /dev/null +++ b/tools/test_apps/system/psram_stack/pytest_psram_stack.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize +from pytest_embedded_idf.utils import soc_filtered_targets + + +@pytest.mark.generic +@pytest.mark.parametrize( + 'config', + [ + 'default', + 'light_sleep', + ], + indirect=True, +) +@idf_parametrize('target', soc_filtered_targets('SOC_SPIRAM_SUPPORTED == 1'), indirect=['target']) +def test_psram_stack(dut: Dut) -> None: + dut.run_all_single_board_cases() diff --git a/tools/test_apps/system/psram_stack/sdkconfig.ci.default b/tools/test_apps/system/psram_stack/sdkconfig.ci.default new file mode 100644 index 00000000000..0a2e0e5ea37 --- /dev/null +++ b/tools/test_apps/system/psram_stack/sdkconfig.ci.default @@ -0,0 +1 @@ +# Base configuration — no additional options beyond sdkconfig.defaults. diff --git a/tools/test_apps/system/psram_stack/sdkconfig.ci.light_sleep b/tools/test_apps/system/psram_stack/sdkconfig.ci.light_sleep new file mode 100644 index 00000000000..ef34b6644d3 --- /dev/null +++ b/tools/test_apps/system/psram_stack/sdkconfig.ci.light_sleep @@ -0,0 +1,4 @@ +CONFIG_PM_ENABLE=y +CONFIG_PM_LIGHT_SLEEP_CALLBACKS=y +CONFIG_FREERTOS_USE_TICKLESS_IDLE=y +CONFIG_PM_DFS_INIT_AUTO=y diff --git a/tools/test_apps/system/psram_stack/sdkconfig.defaults b/tools/test_apps/system/psram_stack/sdkconfig.defaults new file mode 100644 index 00000000000..f6636928a14 --- /dev/null +++ b/tools/test_apps/system/psram_stack/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_SPIRAM=y +CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM=y +CONFIG_FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM=y