diff --git a/components/pthread/include/esp_pthread.h b/components/pthread/include/esp_pthread.h index 32d304a53c..45a58da34d 100644 --- a/components/pthread/include/esp_pthread.h +++ b/components/pthread/include/esp_pthread.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2018-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -20,11 +20,15 @@ extern "C" { /** pthread configuration structure that influences pthread creation */ typedef struct { - size_t stack_size; ///< The stack size of the pthread - size_t prio; ///< The thread's priority - bool inherit_cfg; ///< Inherit this configuration further - const char* thread_name; ///< The thread name. - int pin_to_core; ///< The core id to pin the thread to. Has the same value range as xCoreId argument of xTaskCreatePinnedToCore. + size_t stack_size; /**< The stack size of the pthread */ + size_t prio; /**< The thread's priority */ + bool inherit_cfg; /**< Inherit this configuration further */ + const char* thread_name; /**< The thread name. */ + int pin_to_core; /**< The core id to pin the thread to. Has the same value range as xCoreId + argument of xTaskCreatePinnedToCore. */ + uint32_t stack_alloc_caps; /**< A bit mask of memory capabilities (MALLOC_CAPS*) to use when + allocating the stack. The memory must be 8 bit accessible (MALLOC_CAP_8BIT). + The developer is responsible for the correctenss of \c stack_alloc_caps. */ } esp_pthread_cfg_t; /** @@ -48,6 +52,9 @@ esp_pthread_cfg_t esp_pthread_get_default_config(void); * then the same configuration is also inherited in the thread * subtree. * + * @note If cfg->stack_alloc_caps is 0, it is automatically set to valid default stack memory + * capabilities. If cfg->stack_alloc_caps is non-zero, the developer is responsible for its correctenss. + * This function only checks that the capabilities are MALLOC_CAP_8BIT, the rest is unchecked. * @note Passing non-NULL attributes to pthread_create() will override * the stack_size parameter set using this API * @@ -57,6 +64,7 @@ esp_pthread_cfg_t esp_pthread_get_default_config(void); * - ESP_OK if configuration was successfully set * - ESP_ERR_NO_MEM if out of memory * - ESP_ERR_INVALID_ARG if stack_size is less than PTHREAD_STACK_MIN + * - ESP_ERR_INVALID_ARG if stack_alloc_caps does not include MALLOC_CAP_8BIT */ esp_err_t esp_pthread_set_cfg(const esp_pthread_cfg_t *cfg); diff --git a/components/pthread/pthread.c b/components/pthread/pthread.c index fde33d8a75..d703b1a172 100644 --- a/components/pthread/pthread.c +++ b/components/pthread/pthread.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2018-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2018-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -15,6 +15,10 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" +#if CONFIG_SPIRAM +#include "esp_private/freertos_idf_additions_priv.h" +#endif +#include "esp_heap_caps.h" #include "soc/soc_memory_layout.h" #include "pthread_internal.h" @@ -131,6 +135,19 @@ esp_err_t esp_pthread_set_cfg(const esp_pthread_cfg_t *cfg) return ESP_ERR_INVALID_ARG; } + // 0 is treated as default value, hence change caps to MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL in that case + int heap_caps; + if (cfg->stack_alloc_caps == 0) { + heap_caps = MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL; + } else { + // Check that memory is 8-bit capable + if (!(cfg->stack_alloc_caps & MALLOC_CAP_8BIT)) { + return ESP_ERR_INVALID_ARG; + } + + heap_caps = cfg->stack_alloc_caps; + } + /* If a value is already set, update that value */ esp_pthread_cfg_t *p = pthread_getspecific(s_pthread_cfg_key); if (!p) { @@ -140,6 +157,7 @@ esp_err_t esp_pthread_set_cfg(const esp_pthread_cfg_t *cfg) } } *p = *cfg; + p->stack_alloc_caps = heap_caps; pthread_setspecific(s_pthread_cfg_key, p); return 0; } @@ -167,7 +185,8 @@ esp_pthread_cfg_t esp_pthread_get_default_config(void) .prio = CONFIG_PTHREAD_TASK_PRIO_DEFAULT, .inherit_cfg = false, .thread_name = NULL, - .pin_to_core = get_default_pthread_core() + .pin_to_core = get_default_pthread_core(), + .stack_alloc_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, }; return cfg; @@ -201,6 +220,57 @@ static void pthread_task_func(void *arg) ESP_LOGV(TAG, "%s EXIT", __FUNCTION__); } +#if CONFIG_SPIRAM && CONFIG_FREERTOS_SMP +static UBaseType_t coreID_to_AffinityMask(BaseType_t core_id) +{ + UBaseType_t affinity_mask = tskNO_AFFINITY; + if (core_id != tskNO_AFFINITY) { + affinity_mask = 1 << core_id; + } + return affinity_mask; +} +#endif + +static BaseType_t pthread_create_freertos_task_with_caps(TaskFunction_t pxTaskCode, + const char * const pcName, + const configSTACK_DEPTH_TYPE usStackDepth, + void * const pvParameters, + UBaseType_t uxPriority, + BaseType_t core_id, + UBaseType_t uxStackMemoryCaps, + TaskHandle_t * const pxCreatedTask) +{ +#if CONFIG_SPIRAM + #if CONFIG_FREERTOS_SMP + return prvTaskCreateDynamicAffinitySetWithCaps(pxTaskCode, + pcName, + usStackDepth, + pvParameters, + uxPriority, + coreID_to_AffinityMask(core_id), + uxStackMemoryCaps, + pxCreatedTask); + #else + return prvTaskCreateDynamicPinnedToCoreWithCaps(pxTaskCode, + pcName, + usStackDepth, + pvParameters, + uxPriority, + core_id, + uxStackMemoryCaps, + pxCreatedTask); + #endif +#else + return xTaskCreatePinnedToCore(pxTaskCode, + pcName, + usStackDepth, + pvParameters, + uxPriority, + pxCreatedTask, + core_id); +#endif +} + int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) { @@ -224,6 +294,7 @@ int pthread_create(pthread_t *thread, const pthread_attr_t *attr, BaseType_t prio = CONFIG_PTHREAD_TASK_PRIO_DEFAULT; BaseType_t core_id = get_default_pthread_core(); const char *task_name = CONFIG_PTHREAD_TASK_NAME_DEFAULT; + uint32_t stack_alloc_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; esp_pthread_cfg_t *pthread_cfg = pthread_getspecific(s_pthread_cfg_key); if (pthread_cfg) { @@ -252,6 +323,9 @@ int pthread_create(pthread_t *thread, const pthread_attr_t *attr, core_id = pthread_cfg->pin_to_core; } + // Note: validity has been checked during esp_pthread_set_cfg() + stack_alloc_caps = pthread_cfg->stack_alloc_caps; + task_arg->cfg = *pthread_cfg; } @@ -269,20 +343,23 @@ int pthread_create(pthread_t *thread, const pthread_attr_t *attr, } } + // stack_size is in bytes. This transformation ensures that the units are + // transformed to the units used in FreeRTOS. + // Note: float division of ceil(m / n) == + // integer division of (m + n - 1) / n + stack_size = (stack_size + sizeof(StackType_t) - 1) / sizeof(StackType_t); task_arg->func = start_routine; task_arg->arg = arg; pthread->task_arg = task_arg; - BaseType_t res = xTaskCreatePinnedToCore(&pthread_task_func, + + BaseType_t res = pthread_create_freertos_task_with_caps(&pthread_task_func, task_name, - // stack_size is in bytes. This transformation ensures that the units are - // transformed to the units used in FreeRTOS. - // Note: float division of ceil(m / n) == - // integer division of (m + n - 1) / n - (stack_size + sizeof(StackType_t) - 1) / sizeof(StackType_t), + stack_size, task_arg, prio, - &xHandle, - core_id); + core_id, + stack_alloc_caps, + &xHandle); if (res != pdPASS) { ESP_LOGE(TAG, "Failed to create task!"); diff --git a/components/pthread/test_apps/.build-test-rules.yml b/components/pthread/test_apps/.build-test-rules.yml new file mode 100644 index 0000000000..2856cec83b --- /dev/null +++ b/components/pthread/test_apps/.build-test-rules.yml @@ -0,0 +1,6 @@ +# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps + +components/pthread/test_apps/pthread_psram_tests: + enable: + - if: IDF_TARGET in ["esp32"] + reason: PSRAM only available on ESP32, S2, S3; code is fairly generic diff --git a/components/pthread/test_apps/pthread_psram_tests/CMakeLists.txt b/components/pthread/test_apps/pthread_psram_tests/CMakeLists.txt new file mode 100644 index 0000000000..d1038c898c --- /dev/null +++ b/components/pthread/test_apps/pthread_psram_tests/CMakeLists.txt @@ -0,0 +1,13 @@ +# For more information about build system see +# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +set(COMPONENTS main esp_psram) + +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") # For test_utils component + +project(pthread_psram_tests) diff --git a/components/pthread/test_apps/pthread_psram_tests/README.md b/components/pthread/test_apps/pthread_psram_tests/README.md new file mode 100644 index 0000000000..f708a1985a --- /dev/null +++ b/components/pthread/test_apps/pthread_psram_tests/README.md @@ -0,0 +1,2 @@ +| Supported Targets | ESP32 | +| ----------------- | ----- | diff --git a/components/pthread/test_apps/pthread_psram_tests/main/CMakeLists.txt b/components/pthread/test_apps/pthread_psram_tests/main/CMakeLists.txt new file mode 100644 index 0000000000..f45bed599b --- /dev/null +++ b/components/pthread/test_apps/pthread_psram_tests/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "pthread_psram_tests.c" + INCLUDE_DIRS "." + PRIV_REQUIRES unity test_utils pthread) # note: esp_psram is set in the project's CMakeLists.txt diff --git a/components/pthread/test_apps/pthread_psram_tests/main/pthread_psram_tests.c b/components/pthread/test_apps/pthread_psram_tests/main/pthread_psram_tests.c new file mode 100644 index 0000000000..403e8642a5 --- /dev/null +++ b/components/pthread/test_apps/pthread_psram_tests/main/pthread_psram_tests.c @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "unity.h" +#include "unity_test_runner.h" +#include "memory_checks.h" +#include "esp_heap_caps.h" +#include "esp_pthread.h" +#include +#include "pthread.h" + +void setUp(void) +{ + esp_pthread_cfg_t config = esp_pthread_get_default_config(); + TEST_ASSERT_EQUAL(ESP_OK, esp_pthread_set_cfg(&config)); + test_utils_record_free_mem(); +} + +void tearDown(void) +{ + test_utils_finish_and_evaluate_leaks(0, 0); +} + +TEST_CASE("esp_pthread_get_default_config creates correct stack memory capabilities", "[set_cfg]") +{ + esp_pthread_cfg_t default_config = esp_pthread_get_default_config(); + + // The default must always be internal, 8-bit accessible RAM + TEST_ASSERT_EQUAL_HEX(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL, default_config.stack_alloc_caps); +} + +TEST_CASE("correct memory is accepted", "[set_cfg]") +{ + esp_pthread_cfg_t default_config = esp_pthread_get_default_config(); + + default_config.stack_alloc_caps = MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL; + TEST_ASSERT_EQUAL(ESP_OK, esp_pthread_set_cfg(&default_config)); +} + +TEST_CASE("Setting stack with heap caps 0 sets the default value", "[set_cfg]") +{ + esp_pthread_cfg_t config = { .stack_size = 4096 }; // all other values are set to 0 + + TEST_ASSERT_EQUAL(ESP_OK, esp_pthread_set_cfg(&config)); + + TEST_ASSERT_EQUAL(ESP_OK, esp_pthread_get_cfg(&config)); + TEST_ASSERT_EQUAL(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL, config.stack_alloc_caps); +} + +TEST_CASE("Setting stack with non 8-bit caps fails", "[set_cfg]") +{ + esp_pthread_cfg_t config = esp_pthread_get_default_config(); + config.stack_alloc_caps = MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL; + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_pthread_set_cfg(&config)); +} + +static void *check_stack_in_spiram(void *arg) +{ + int ret_value; + if (esp_ptr_internal(&ret_value)) { + ret_value = 0; + } else { + ret_value = 1; + } + vTaskDelay(2); // ensure the test task has time to continue execution + pthread_exit((void *) ret_value); + return NULL; +} + +TEST_CASE("pthread_create fails because out of PSRAM", "[psram]") +{ + esp_pthread_cfg_t config = esp_pthread_get_default_config(); + config.stack_alloc_caps = MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM; + config.stack_size = 0xFFFFFFFF; // far larger than the virtual address space on ESP32 + pthread_t pthread_object = (pthread_t)NULL; + TEST_ASSERT_EQUAL(ESP_OK, esp_pthread_set_cfg(&config)); + + TEST_ASSERT_EQUAL(ENOMEM, pthread_create(&pthread_object, NULL, check_stack_in_spiram, NULL)); +} + +TEST_CASE("pthread create large PSRAM stack", "[psram]") +{ + esp_pthread_cfg_t config = esp_pthread_get_default_config(); + config.stack_alloc_caps = MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM; + config.stack_size = 0x80000; // value is too large for any internal RAM on current chips + int res = -1; + int thread_rval = -1; + pthread_t pthread_object = (pthread_t)NULL; + + TEST_ASSERT_EQUAL(ESP_OK, esp_pthread_set_cfg(&config)); + + res = pthread_create(&pthread_object, NULL, check_stack_in_spiram, NULL); + TEST_ASSERT_EQUAL_INT(0, res); + + res = pthread_join(pthread_object, (void*) &thread_rval); + TEST_ASSERT_EQUAL_INT(0, res); + TEST_ASSERT_EQUAL_INT(1, thread_rval); + + // Add a short delay to allow the idle task to free any remaining memory + vTaskDelay(2); +} + +TEST_CASE("pthread with stack in internal RAM", "[psram]") +{ + int res = -1; + int thread_rval = -1; + pthread_t pthread_object = (pthread_t)NULL; + + res = pthread_create(&pthread_object, NULL, check_stack_in_spiram, NULL); + TEST_ASSERT_EQUAL_INT(0, res); + + res = pthread_join(pthread_object, (void*) &thread_rval); + TEST_ASSERT_EQUAL_INT(0, res); + TEST_ASSERT_EQUAL_INT(0, thread_rval); + + // Add a short delay to allow the idle task to free any remaining memory + vTaskDelay(2); +} + +void app_main(void) +{ + vTaskPrioritySet(NULL, CONFIG_UNITY_FREERTOS_PRIORITY); + printf("pthread PSRAM Test"); + unity_run_menu(); +} diff --git a/components/pthread/test_apps/pthread_psram_tests/pytest_pthread_psram_tests.py b/components/pthread/test_apps/pthread_psram_tests/pytest_pthread_psram_tests.py new file mode 100644 index 0000000000..f5b576027b --- /dev/null +++ b/components/pthread/test_apps/pthread_psram_tests/pytest_pthread_psram_tests.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +@pytest.mark.esp32 +def test_pthread_psram(dut: Dut) -> None: + dut.run_all_single_board_cases(timeout=10) diff --git a/components/pthread/test_apps/pthread_psram_tests/sdkconfig.defaults b/components/pthread/test_apps/pthread_psram_tests/sdkconfig.defaults new file mode 100644 index 0000000000..54d789fd40 --- /dev/null +++ b/components/pthread/test_apps/pthread_psram_tests/sdkconfig.defaults @@ -0,0 +1,5 @@ +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 +CONFIG_ESP_TASK_WDT_INIT=n +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y +CONFIG_SPIRAM=y diff --git a/components/pthread/test_apps/pthread_unity_tests/main/test_pthread.c b/components/pthread/test_apps/pthread_unity_tests/main/test_pthread.c index 675d4208b2..a85b03b4dd 100644 --- a/components/pthread/test_apps/pthread_unity_tests/main/test_pthread.c +++ b/components/pthread/test_apps/pthread_unity_tests/main/test_pthread.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -13,6 +13,33 @@ #include "unity.h" +TEST_CASE("esp_pthread_get_default_config creates correct stack memory capabilities", "[set_cfg]") +{ + esp_pthread_cfg_t default_config = esp_pthread_get_default_config(); + + // The default must always be internal, 8-bit accessible RAM + TEST_ASSERT_EQUAL_HEX(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL, default_config.stack_alloc_caps); +} + +TEST_CASE("wrong heap caps are rejected", "[set_cfg]") +{ + esp_pthread_cfg_t default_config = esp_pthread_get_default_config(); + + default_config.stack_alloc_caps = MALLOC_CAP_32BIT; + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_pthread_set_cfg(&default_config)); + + default_config.stack_alloc_caps = MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL; + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_pthread_set_cfg(&default_config)); +} + +TEST_CASE("correct memory is accepted", "[set_cfg]") +{ + esp_pthread_cfg_t default_config = esp_pthread_get_default_config(); + + default_config.stack_alloc_caps = MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL; + TEST_ASSERT_EQUAL(ESP_OK, esp_pthread_set_cfg(&default_config)); +} + static void *compute_square(void *arg) { int *num = (int *) arg; diff --git a/docs/en/api-reference/system/mem_alloc.rst b/docs/en/api-reference/system/mem_alloc.rst index 7bfb9296d3..f4f47ce0c6 100644 --- a/docs/en/api-reference/system/mem_alloc.rst +++ b/docs/en/api-reference/system/mem_alloc.rst @@ -14,6 +14,8 @@ Because {IDF_TARGET_NAME} uses multiple types of RAM, it also contains multiple For most purposes, the C Standard Library's ``malloc()`` and ``free()`` functions can be used for heap allocation without any special consideration. However, in order to fully make use of all of the memory types and their characteristics, ESP-IDF also has a capabilities-based heap memory allocator. If you want to have a memory with certain properties (e.g., :ref:`dma-capable-memory` or executable-memory), you can create an OR-mask of the required capabilities and pass that to :cpp:func:`heap_caps_malloc`. +.. _memory_capabilities: + Memory Capabilities ------------------- diff --git a/docs/en/api-reference/system/pthread.rst b/docs/en/api-reference/system/pthread.rst index ec79ca8d62..14a68ad679 100644 --- a/docs/en/api-reference/system/pthread.rst +++ b/docs/en/api-reference/system/pthread.rst @@ -188,6 +188,7 @@ The API :cpp:func:`esp_pthread_set_cfg` defined in the ``esp_pthreads.h`` header .. list:: - Default stack size of new threads, if not specified when calling ``pthread_create()`` (overrides :ref:`CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT`). + - Stack memory capabilities: They determine which kind of memory is used for allocating pthread stacks. The field takes ESP-IDF heap capability flags, as defined in :component_file:`heap/include/esp_heap_caps.h`. The memory must be 8-bit accessible (MALLOC_CAP_8BIT), besides other custom flags the user can choose from. The user is responsible for ensuring the correctness of the stack memory capabilities. For more information about memory locations, refer to the documentation of :ref:`Memory Capabilities ` - RTOS priority of new threads (overrides :ref:`CONFIG_PTHREAD_TASK_PRIO_DEFAULT`). :SOC_HP_CPU_HAS_MULTIPLE_CORES: - Core affinity / core pinning of new threads (overrides :ref:`CONFIG_PTHREAD_TASK_CORE_DEFAULT`). - FreeRTOS task name for new threads (overrides :ref:`CONFIG_PTHREAD_TASK_NAME_DEFAULT`) diff --git a/docs/zh_CN/api-reference/system/mem_alloc.rst b/docs/zh_CN/api-reference/system/mem_alloc.rst index 5158750ae5..768165115d 100644 --- a/docs/zh_CN/api-reference/system/mem_alloc.rst +++ b/docs/zh_CN/api-reference/system/mem_alloc.rst @@ -14,6 +14,8 @@ ESP-IDF 应用程序使用常见的计算机架构模式:由程序控制流动 多数情况下,可直接使用 C 标准函数库中的 ``malloc()`` 和 ``free()`` 函数实现堆分配。为充分利用各种内存类型及其特性,ESP-IDF 还具有基于内存属性的堆内存分配器。要配备具有特定属性的内存,如 :ref:`dma-capable-memory` 或可执行内存,可以创建具备所需属性的 OR 掩码,将其传递给 :cpp:func:`heap_caps_malloc`。 +.. _memory_capabilities: + 内存属性 -------------------