diff --git a/components/bt/CMakeLists.txt b/components/bt/CMakeLists.txt index cfe74f1eec4..676d0cfa9b3 100644 --- a/components/bt/CMakeLists.txt +++ b/components/bt/CMakeLists.txt @@ -61,6 +61,11 @@ if(CONFIG_IDF_DOC_BUILD OR CONFIG_BT_ENABLED) list(APPEND srcs ${ble_mesh_srcs}) list(APPEND include_dirs ${ble_mesh_include_dirs}) + # BLE profiles (host-agnostic profile cores + shared profile infrastructure) + add_subdirectory(ble_profiles) + list(APPEND srcs ${ble_profiles_srcs}) + list(APPEND include_dirs ${ble_profiles_include_dirs}) + # When log compression is enabled, selected logs are replaced # by auto-generated macros that emit pre-encoded data. # This eliminates the original format strings, reducing firmware size and diff --git a/components/bt/Kconfig b/components/bt/Kconfig index 0bc00108f12..50aa735f9ed 100644 --- a/components/bt/Kconfig +++ b/components/bt/Kconfig @@ -115,6 +115,10 @@ menu "Bluetooth" help Enable this to allow the BT stack to send diagnostic events. + menu "BLE Profiles" + depends on BT_ENABLED + source "$IDF_PATH/components/bt/ble_profiles/Kconfig.in" + endmenu endmenu diff --git a/components/bt/ble_profiles/CMakeLists.txt b/components/bt/ble_profiles/CMakeLists.txt new file mode 100644 index 00000000000..0b9405fc074 --- /dev/null +++ b/components/bt/ble_profiles/CMakeLists.txt @@ -0,0 +1,22 @@ +# BLE profiles: host-agnostic profile cores and the shared profile +# infrastructure they build on (e.g. the shared profile task in common/). +# + +set(ble_profiles_srcs "" PARENT_SCOPE) +set(ble_profiles_include_dirs "" PARENT_SCOPE) + +if(NOT CONFIG_BT_ENABLED) + return() +endif() + +set(_srcs "") +# The header is always visible so profiles can include it unconditionally; the +# implementation is compiled only when the shared task is enabled. +set(_include_dirs "${CMAKE_CURRENT_LIST_DIR}/common/include") + +if(CONFIG_BT_PRF_TASK_ENABLED) + list(APPEND _srcs "${CMAKE_CURRENT_LIST_DIR}/common/src/bt_prf_task.c") +endif() + +set(ble_profiles_srcs "${_srcs}" PARENT_SCOPE) +set(ble_profiles_include_dirs "${_include_dirs}" PARENT_SCOPE) diff --git a/components/bt/ble_profiles/Kconfig.in b/components/bt/ble_profiles/Kconfig.in new file mode 100644 index 00000000000..a8be997a9b1 --- /dev/null +++ b/components/bt/ble_profiles/Kconfig.in @@ -0,0 +1,7 @@ +# BLE profiles: aggregation point for the host-agnostic profile cores and the +# shared profile infrastructure they build on. Each profile (and the shared +# infrastructure in common/) contributes its own Kconfig.in here, so the +# top-level bt/Kconfig only ever needs to source this file. +# + +source "$IDF_PATH/components/bt/ble_profiles/common/Kconfig.in" diff --git a/components/bt/ble_profiles/common/Kconfig.in b/components/bt/ble_profiles/common/Kconfig.in new file mode 100644 index 00000000000..eae3f0eb7a3 --- /dev/null +++ b/components/bt/ble_profiles/common/Kconfig.in @@ -0,0 +1,45 @@ +config BT_PRF_TASK_ENABLED + bool "Enable the shared BLE profile task (event queue worker)" + depends on BT_ENABLED + default n + help + Build the shared BLE profile task: a single process-wide event queue + served by one dedicated FreeRTOS task, onto which BLE profiles post + short, non-blocking handlers instead of each spawning its own task. + + Host-agnostic (depends only on the BT OSAL and FreeRTOS), so both the + Bluedroid and NimBLE hosts can use it. A profile that relies on the + shared task should "select BT_PRF_TASK_ENABLED" in its own Kconfig. + +config BT_PRF_TASK_STACK_SIZE + int "Shared BLE profile task stack size (bytes)" + depends on BT_PRF_TASK_ENABLED + range 2048 16384 + default 3072 + help + Stack size, in bytes, of the shared BLE profile task. It must be large + enough for the deepest profile handler posted to the queue; raise it if + a handler needs more stack. + +config BT_PRF_TASK_PRIORITY + int "Shared BLE profile task priority" + depends on BT_PRF_TASK_ENABLED + range 1 24 + default 5 + help + FreeRTOS priority of the shared BLE profile task. Keep it below the BLE + host task so profile handling never starves the protocol stack. + +config BT_PRF_TASK_CORE_ID + int "Shared BLE profile task core affinity" + depends on BT_PRF_TASK_ENABLED + range -1 0 if FREERTOS_UNICORE + range -1 1 + default 0 + help + Core the shared BLE profile task is pinned to: 0 for CPU0, 1 for CPU1, + or -1 to leave it unpinned so the scheduler may run it on either core. + + On a single-core target the only valid values are 0 and -1. Pinning the + task to the core that does not run the BLE host can keep profile + handlers from competing with the protocol stack for CPU time. diff --git a/components/bt/ble_profiles/common/include/bt_prf_task.h b/components/bt/ble_profiles/common/include/bt_prf_task.h new file mode 100644 index 00000000000..1b5f8ed0e43 --- /dev/null +++ b/components/bt/ble_profiles/common/include/bt_prf_task.h @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file bt_prf_task.h + * @brief Shared BLE profile task (event queue worker) + * + * A single, process-wide event queue served by one dedicated FreeRTOS task, + * shared by BLE profiles that need to run their business logic off the host + * task. Profiles post short, non-blocking handlers here instead of each + * spawning its own task, which keeps task count and RAM bounded. + * + * Profiles post work with bt_prf_task_eventq(): obtain the underlying queue and + * drive it directly with a caller-owned ::bt_osal_event embedded in the profile's + * own context. This avoids per-post allocation and lets an in-flight event + * coalesce (an already-queued event is never enqueued twice), and it is the + * queue to bind a ::bt_osal_callout to for delayed work. + * + * Built only when CONFIG_BT_PRF_TASK_ENABLED is set. A profile that relies on + * the shared task should `select BT_PRF_TASK_ENABLED` in its own Kconfig. + * + * @note This is the profile-facing policy layer on top of the OS-primitive OSAL + * (see bt_osal.h). The handler runs in the shared task's context, so it + * MUST be non-blocking and quick; hand long or blocking work off to a + * dedicated task. + */ + +#pragma once + +#include "bt_osal.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Bring up the shared BLE profile task + * + * @note The active host bring-up already calls this (esp_bluedroid_init() / + * nimble_port_init()), so profiles do NOT need to. Exposed mainly for tests. + */ +bt_osal_error_t bt_prf_task_init(void); + +/** + * @brief Tear down the shared BLE profile task + * + * @note The active host tear-down already calls this (esp_bluedroid_deinit() / + * nimble_port_deinit()), so profiles do NOT need to. Exposed mainly for tests. + */ +bt_osal_error_t bt_prf_task_deinit(void); + +/** + * @brief Get the shared profile task's event queue + * + * Use it to drive the queue directly with a caller-owned event (zero-allocation + * posting via bt_osal_eventq_put()) or to bind a ::bt_osal_callout for delayed + * work that runs in the shared task's context. + * + * @return Pointer to the shared event queue, or NULL if the task is not running + */ +struct bt_osal_eventq *bt_prf_task_eventq(void); + +/** + * @brief Check whether the shared BLE profile task is running + * + * @return true if bt_prf_task_init() has succeeded and the task is up + */ +bool bt_prf_task_is_running(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/bt/ble_profiles/common/src/bt_prf_task.c b/components/bt/ble_profiles/common/src/bt_prf_task.c new file mode 100644 index 00000000000..dcd996c82ef --- /dev/null +++ b/components/bt/ble_profiles/common/src/bt_prf_task.c @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "sdkconfig.h" +#include "esp_log.h" +#include "bt_osal.h" +#include "bt_osal_freertos.h" +#include "bt_prf_task.h" + +static const char *TAG = "bt_prf_task"; + +/** + * @brief Internal state of the shared BLE profile task + * + * Allocated in bt_prf_task_init() and freed in bt_prf_task_deinit(). The + * pointer @ref s_prf_task is written only during single-threaded bring-up and + * tear-down and read (never written) by bt_prf_task_post(); the documented + * lifecycle contract (init happens-before any post, deinit happens-after the + * last post) makes it safe without a lock. + */ +typedef struct { + struct bt_osal_eventq evq; /*!< Shared event queue served by the worker task */ + bool running; /*!< true once the worker task has been started */ +} bt_prf_task_ctx_t; + +static bt_prf_task_ctx_t *s_prf_task; + +bt_osal_error_t bt_prf_task_init(void) +{ + bt_prf_task_ctx_t *ctx; + bt_osal_error_t rc; + + /* Idempotent: a second bring-up while already running is a no-op. */ + if (s_prf_task != NULL) { + return BT_OSAL_OK; + } + + ctx = calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + ESP_LOGE(TAG, "no memory for profile task context"); + return BT_OSAL_ENOMEM; + } + + /* Allocates the queue's backing storage; asserts internally on OOM. */ + bt_osal_eventq_init(&ctx->evq); + + /* Kconfig encodes "any core" as -1; the OSAL wants its own sentinel. */ + struct bt_osal_task_info task_info = { + .name = "bt_profiles", + .prio = CONFIG_BT_PRF_TASK_PRIORITY, + .stack_size = CONFIG_BT_PRF_TASK_STACK_SIZE, + .core_id = (CONFIG_BT_PRF_TASK_CORE_ID < 0) ? BT_OSAL_TASK_NO_AFFINITY + : CONFIG_BT_PRF_TASK_CORE_ID, + }; + rc = bt_osal_eventq_start(&ctx->evq, &task_info); + if (rc != BT_OSAL_OK) { + ESP_LOGE(TAG, "failed to start profile task: %d", rc); + bt_osal_eventq_deinit(&ctx->evq); + free(ctx); + return rc; + } + + ctx->running = true; + s_prf_task = ctx; + + ESP_LOGI(TAG, "profile task started (stack %d, prio %d)", + CONFIG_BT_PRF_TASK_STACK_SIZE, CONFIG_BT_PRF_TASK_PRIORITY); + return BT_OSAL_OK; +} + +bt_osal_error_t bt_prf_task_deinit(void) +{ + bt_prf_task_ctx_t *ctx = s_prf_task; + + /* Symmetric no-op if bring-up never happened. */ + if (ctx == NULL) { + return BT_OSAL_OK; + } + + /* Publish the torn-down state before releasing resources so a stray + * bt_prf_task_post() observes "not running" rather than a dangling queue. */ + s_prf_task = NULL; + + /* Stops (deletes) the worker task, then frees the queue's storage. */ + bt_osal_eventq_deinit(&ctx->evq); + free(ctx); + + ESP_LOGI(TAG, "profile task stopped"); + return BT_OSAL_OK; +} + +struct bt_osal_eventq *bt_prf_task_eventq(void) +{ + bt_prf_task_ctx_t *ctx = s_prf_task; + + return (ctx != NULL && ctx->running) ? &ctx->evq : NULL; +} + +bool bt_prf_task_is_running(void) +{ + bt_prf_task_ctx_t *ctx = s_prf_task; + + return ctx != NULL && ctx->running; +} diff --git a/components/bt/common/CMakeLists.txt b/components/bt/common/CMakeLists.txt index 062a4498da8..4bbf49754ea 100644 --- a/components/bt/common/CMakeLists.txt +++ b/components/bt/common/CMakeLists.txt @@ -114,6 +114,7 @@ list(APPEND bt_common_srcs "${CMAKE_CURRENT_LIST_DIR}/osi/osi.c" "${CMAKE_CURRENT_LIST_DIR}/osi/semaphore.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/deprecated/ble_log_spi_out.c" + "${CMAKE_CURRENT_LIST_DIR}/osal/src/bt_osal_freertos.c" ) list(APPEND bt_common_include_dirs @@ -124,6 +125,7 @@ list(APPEND bt_common_include_dirs "${CMAKE_CURRENT_LIST_DIR}/hci_log/include" "${CMAKE_CURRENT_LIST_DIR}/ble_log/include" "${CMAKE_CURRENT_LIST_DIR}/ble_log/deprecated/include" + "${CMAKE_CURRENT_LIST_DIR}/osal/include" ) list(APPEND bt_common_priv_include_dirs diff --git a/components/bt/common/Kconfig.in b/components/bt/common/Kconfig.in index 2c0b992e86a..1ef7bf43dfe 100644 --- a/components/bt/common/Kconfig.in +++ b/components/bt/common/Kconfig.in @@ -21,6 +21,21 @@ config BT_BLE_HOST_ALLOW_SUB_SPEC_MIN_CONN_INT this mode; use the Controller's own configuration instead of toggling this host symbol manually. +menu "OSAL (OS abstraction layer)" + depends on BT_ENABLED + + config BT_OSAL_USE_ESP_TIMER + bool "Use esp_timer to implement the OSAL callout" + default y + help + Use esp_timer instead of the FreeRTOS software timer to implement the OSAL + callout. esp_timer offers higher precision and runs its callbacks in a + dedicated high-priority task rather than the shared FreeRTOS timer task. + + Disable to fall back to the FreeRTOS software timer. + +endmenu + choice BT_SMP_CRYPTO_STACK prompt "SMP cryptographic stack" depends on (BT_BLE_SMP_ENABLE || BT_SMP_ENABLE || BT_NIMBLE_SECURITY_ENABLE || BT_LE_SECURITY_ENABLE) diff --git a/components/bt/common/osal/include/bt_osal.h b/components/bt/common/osal/include/bt_osal.h new file mode 100644 index 00000000000..b4322bb18bc --- /dev/null +++ b/components/bt/common/osal/include/bt_osal.h @@ -0,0 +1,686 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file bt_osal.h + * @brief OS Abstraction Layer (OSAL) for the Bluetooth host + * + * Provides the OS primitives the Bluetooth host relies on: event queues, + * mutexes, semaphores, callouts (one-shot timers), time conversion and + * critical sections. + * + * Every API declared here dispatches through the function table installed by + * bt_osal_freertos_funcs_init(), which MUST be called before any other bt_osal_* + * API is used. + * + */ + +#ifndef _BT_OSAL_H_ +#define _BT_OSAL_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct bt_osal_event; + +/** + * @brief Event handler invoked when a queued event is dispatched + * + * @param ev Event being dispatched. Use bt_osal_event_get_arg() to retrieve the + * argument registered at bt_osal_event_init() time. + */ +typedef void bt_osal_event_fn(struct bt_osal_event *ev); + +/** + * @brief OSAL error codes + */ +enum bt_osal_error { + BT_OSAL_OK = 0, /*!< Success */ + BT_OSAL_ENOMEM = 1, /*!< Out of memory */ + BT_OSAL_EINVAL = 2, /*!< Invalid argument or invalid object state */ + BT_OSAL_INVALID_PARAM = 3, /*!< Object is not initialized, or allocation for it failed */ + BT_OSAL_MEM_NOT_ALIGNED = 4, /*!< Memory is not aligned as required */ + BT_OSAL_BAD_MUTEX = 5, /*!< Mutex is not owned by the calling task */ + BT_OSAL_TIMEOUT = 6, /*!< Operation timed out before it could complete */ + BT_OSAL_ERR_IN_ISR = 7, /*!< Operation is not allowed from interrupt context */ + BT_OSAL_ERR_PRIV = 8, /*!< Operation requires privileges the caller does not have */ + BT_OSAL_OS_NOT_STARTED = 9, /*!< The OS scheduler has not been started yet */ + BT_OSAL_ENOENT = 10, /*!< Requested object does not exist */ + BT_OSAL_EBUSY = 11, /*!< Object is busy */ + BT_OSAL_ERROR = 12, /*!< Unspecified error */ +}; + +/** + * @brief OSAL error code type + */ +typedef enum bt_osal_error bt_osal_error_t; + +/* Include OS-specific definitions */ +#include "bt_osal_os.h" + +/* + * Generic + */ + +/** + * @brief Check whether the OS scheduler is running + * + * @return true if the scheduler has been started, false otherwise + */ +bool bt_osal_os_started(void); + +/** + * @brief Get an opaque identifier of the calling task + * + * @return Handle of the calling task + */ +void *bt_osal_get_current_task_id(void); + +/* + * Event queue + */ + +/** + * @brief Initialize an event queue + * + * Allocates the queue's backing storage on first use. Calling this on an + * already-initialized queue reuses the storage and discards any pending events, + * leaving the queue empty. + * + * @note Allocates memory, so it must not be called from interrupt context. + * + * @param evq Event queue to initialize + */ +void bt_osal_eventq_init(struct bt_osal_eventq *evq); + +/** + * @brief Deinitialize an event queue and release its resources + * + * Deletes the queue's processing task if one was started, then frees the + * backing storage. It is safe to call this on a queue that was never + * initialized or already deinitialized. + * + * @param evq Event queue to deinitialize + */ +void bt_osal_eventq_deinit(struct bt_osal_eventq *evq); + +/** + * @brief Dequeue the next event, waiting for one if the queue is empty + * + * The returned event is no longer marked as queued, so it may be posted again. + * + * @note From interrupt context @p tmo MUST be 0; a non-zero timeout asserts. + * + * @param evq Event queue to take the event from + * @param tmo Maximum time to wait, in ticks. Use 0 to poll, or + * BT_OSAL_TIME_FOREVER to wait indefinitely. + * + * @return Next event in the queue, or NULL if none became available within @p tmo + */ +struct bt_osal_event *bt_osal_eventq_get(struct bt_osal_eventq *evq, + bt_osal_time_t tmo); + +/** + * @brief Append an event to the back of an event queue + * + * Does nothing if @p ev is already queued, so an event is never enqueued twice. + * + * @param evq Event queue to post to + * @param ev Event to post + */ +void bt_osal_eventq_put(struct bt_osal_eventq *evq, struct bt_osal_event *ev); + +/** + * @brief Insert an event at the front of an event queue + * + * Same as bt_osal_eventq_put() except the event is dequeued before all events + * already pending. Does nothing if @p ev is already queued. + * + * @param evq Event queue to post to + * @param ev Event to post + */ +void bt_osal_eventq_put_to_front(struct bt_osal_eventq *evq, + struct bt_osal_event *ev); + +/** + * @brief Remove a pending event from an event queue + * + * Does nothing if @p ev is not currently queued. + * + * @param evq Event queue holding the event + * @param ev Event to remove + */ +void bt_osal_eventq_remove(struct bt_osal_eventq *evq, + struct bt_osal_event *ev); + +/** + * @brief Start a dedicated task that processes an event queue + * + * Spawns a task that blocks on bt_osal_eventq_get() and dispatches each dequeued + * event with bt_osal_event_run(), so events posted to @p evq are handled + * asynchronously in this task's context. The queue must already be initialized + * with bt_osal_eventq_init(). + * + * @note Only one processing task may be started per queue. + * + * @param evq Event queue to process + * @param info Attributes (name, priority, stack size, core affinity) of the + * processing task; @c info->name must not be NULL. Set + * @c info->core_id to BT_OSAL_TASK_NO_AFFINITY to leave the task + * unpinned — a zero-initialized @p info pins it to core 0. + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_EINVAL if @p info or @c info->name is NULL, @c info->core_id is + * neither a valid core nor BT_OSAL_TASK_NO_AFFINITY, the queue is not + * initialized, or it already has a processing task + * - BT_OSAL_ENOMEM if the task could not be created + */ +bt_osal_error_t bt_osal_eventq_start(struct bt_osal_eventq *evq, + const struct bt_osal_task_info *info); + +/** + * @brief Post a one-shot function to be run from an event queue + * + * Allocates a self-contained event, posts it to @p evq, and runs @p fn when the + * event is dispatched by the queue's processing task (see bt_osal_eventq_start()). + * The event's backing storage is freed automatically after @p fn returns, so no + * event object needs to be managed by the caller. Inside @p fn the argument + * @p arg is retrievable via bt_osal_event_get_arg(). + * + * @note Allocates memory, so it must not be called from interrupt context. + * + * @param evq Event queue to post the work to + * @param fn Handler to run when the event is dispatched + * @param arg Argument passed to @p fn + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_EINVAL if the queue is not initialized or @p fn is NULL + * - BT_OSAL_ERR_IN_ISR if called from interrupt context + * - BT_OSAL_ENOMEM if the work item could not be allocated + */ +bt_osal_error_t bt_osal_eventq_post_func(struct bt_osal_eventq *evq, + bt_osal_event_fn *fn, void *arg); + +/** + * @brief Initialize an event with its handler and argument + * + * Allocates the event's backing storage on first use. Calling this on an + * already-initialized event reuses the storage and overwrites the handler and + * argument. + * + * @note Allocates memory, so it must not be called from interrupt context. + * + * @param ev Event to initialize + * @param fn Handler to run when the event is dispatched + * @param arg Argument passed to @p fn, retrievable via bt_osal_event_get_arg() + */ +void bt_osal_event_init(struct bt_osal_event *ev, bt_osal_event_fn *fn, + void *arg); + +/** + * @brief Deinitialize an event and free its backing storage + * + * The event must not be queued; remove it with bt_osal_eventq_remove() first. + * It is safe to call this on an event that was never initialized or already + * deinitialized. + * + * @param ev Event to deinitialize + */ +void bt_osal_event_deinit(struct bt_osal_event *ev); + +/** + * @brief Clear the queued flag of an event + * + * Only clears the flag; it does NOT unlink the event from a queue it is still + * pending on. To take a queued event out of its queue, use + * bt_osal_eventq_remove(). + * + * @warning The event MUST NOT be queued when this is called. Resetting a still- + * queued event desynchronizes its flag from the queue and, with the + * intrusive-list backend, leaves it linked while marked free — a later + * put() then relinks the same node and corrupts the list. Callers must + * bt_osal_eventq_remove() (or let it be dispatched) first; the flag is + * checked with an assertion in debug builds. + * + * @param ev Event to reset (must not be currently queued) + */ +void bt_osal_event_reset(struct bt_osal_event *ev); + +/** + * @brief Check whether an event is pending in an event queue + * + * @param ev Event to query + * + * @return true if the event is queued and not yet dispatched, false otherwise + */ +bool bt_osal_event_is_queued(struct bt_osal_event *ev); + +/** + * @brief Get the argument registered with an event + * + * @param ev Event to query + * + * @return Argument passed to bt_osal_event_init() or bt_osal_event_set_arg() + */ +void *bt_osal_event_get_arg(struct bt_osal_event *ev); + +/** + * @brief Replace the argument registered with an event + * + * @param ev Event to update + * @param arg New argument to pass to the event handler + */ +void bt_osal_event_set_arg(struct bt_osal_event *ev, void *arg); + +/** + * @brief Check whether an event queue holds no pending events + * + * @param evq Event queue to query + * + * @return true if the queue is empty, false otherwise + */ +bool bt_osal_eventq_is_empty(struct bt_osal_eventq *evq); + +/** + * @brief Run an event's handler in the calling context + * + * Invokes the handler registered by bt_osal_event_init() synchronously; the + * event is not required to have been queued. + * + * @param ev Event to dispatch + */ +void bt_osal_event_run(struct bt_osal_event *ev); + +/* + * Mutexes + */ + +/** + * @brief Initialize a recursive mutex + * + * Allocates the mutex on first use; calling this on an already-initialized + * mutex leaves it untouched and succeeds. + * + * @note Allocates memory, so it must not be called from interrupt context. + * + * @param mu Mutex to initialize + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_ENOMEM if the mutex could not be allocated + */ +bt_osal_error_t bt_osal_mutex_init(struct bt_osal_mutex *mu); + +/** + * @brief Acquire a mutex, waiting for it if it is held by another task + * + * The mutex is recursive: a task already owning it acquires it again without + * blocking, and must release it once per successful pend. + * + * @note Must not be called from interrupt context; doing so asserts. + * + * @param mu Mutex to acquire + * @param timeout Maximum time to wait, in ticks. Use 0 to try without blocking, + * or BT_OSAL_TIME_FOREVER to wait indefinitely. + * + * @return + * - BT_OSAL_OK if the mutex was acquired + * - BT_OSAL_TIMEOUT if it was still held when @p timeout elapsed + * - BT_OSAL_INVALID_PARAM if the mutex is not initialized + */ +bt_osal_error_t bt_osal_mutex_pend(struct bt_osal_mutex *mu, + bt_osal_time_t timeout); + +/** + * @brief Release a mutex held by the calling task + * + * A recursively acquired mutex is only released to other tasks once this has + * been called as many times as it was successfully pended. + * + * @note Must not be called from interrupt context; doing so asserts. + * + * @param mu Mutex to release + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_BAD_MUTEX if the calling task does not own the mutex + * - BT_OSAL_INVALID_PARAM if the mutex is not initialized + */ +bt_osal_error_t bt_osal_mutex_release(struct bt_osal_mutex *mu); + +/** + * @brief Deinitialize a mutex and free its resources + * + * The mutex must not be held when it is deinitialized. + * + * @param mu Mutex to deinitialize + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_INVALID_PARAM if the mutex is not initialized + */ +bt_osal_error_t bt_osal_mutex_deinit(struct bt_osal_mutex *mu); + +/* + * Semaphores + */ + +/** + * @brief Initialize a counting semaphore + * + * Allocates the semaphore on first use; calling this on an already-initialized + * semaphore leaves it untouched (including its token count) and succeeds. + * + * @note Allocates memory, so it must not be called from interrupt context. + * + * @param sem Semaphore to initialize + * @param tokens Initial number of tokens + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_ENOMEM if the semaphore could not be allocated + */ +bt_osal_error_t bt_osal_sem_init(struct bt_osal_sem *sem, uint16_t tokens); + +/** + * @brief Take one token from a semaphore, waiting if none is available + * + * @note From interrupt context @p timeout MUST be 0; a non-zero timeout asserts. + * + * @param sem Semaphore to take a token from + * @param timeout Maximum time to wait, in ticks. Use 0 to try without blocking, + * or BT_OSAL_TIME_FOREVER to wait indefinitely. + * + * @return + * - BT_OSAL_OK if a token was taken + * - BT_OSAL_TIMEOUT if no token became available within @p timeout + * - BT_OSAL_INVALID_PARAM if the semaphore is not initialized + */ +bt_osal_error_t bt_osal_sem_pend(struct bt_osal_sem *sem, + bt_osal_time_t timeout); + +/** + * @brief Return one token to a semaphore, unblocking a waiter if any + * + * @param sem Semaphore to give a token to + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_INVALID_PARAM if the semaphore is not initialized + * - BT_OSAL_ERROR if the token could not be returned (semaphore already full) + */ +bt_osal_error_t bt_osal_sem_release(struct bt_osal_sem *sem); + +/** + * @brief Deinitialize a semaphore and free its resources + * + * No task may be pending on the semaphore when it is deinitialized. + * + * @param sem Semaphore to deinitialize + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_INVALID_PARAM if the semaphore is not initialized + */ +bt_osal_error_t bt_osal_sem_deinit(struct bt_osal_sem *sem); + +/** + * @brief Get the number of tokens currently held by a semaphore + * + * @param sem Semaphore to query + * + * @return Current token count + */ +uint16_t bt_osal_sem_get_count(struct bt_osal_sem *sem); + +/* + * Callouts + */ + +/** + * @brief Initialize a callout (one-shot timer) + * + * The callout is created stopped; arm it with bt_osal_callout_reset(). + * + * On expiry, if @p evq is non-NULL the callout's event is posted to that queue + * and @p ev_cb runs in the context of the task processing the queue. If @p evq + * is NULL, @p ev_cb is instead run directly in timer context, where it must not + * block and must keep its stack usage small. + * + * Allocates the callout's backing storage on first use; calling this on an + * already-initialized callout reuses the storage and rebinds the queue, handler + * and argument. + * + * @note Allocates memory, so it must not be called from interrupt context. + * + * @param co Callout to initialize + * @param evq Event queue to post the event to on expiry, or NULL to run + * @p ev_cb in timer context + * @param ev_cb Handler to run when the callout expires + * @param ev_arg Argument passed to @p ev_cb + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_ENOMEM if the callout or its underlying timer could not be created + */ +bt_osal_error_t bt_osal_callout_init(struct bt_osal_callout *co, struct bt_osal_eventq *evq, + bt_osal_event_fn *ev_cb, void *ev_arg); + +/** + * @brief Stop a callout, delete its timer and free its resources + * + * It is safe to call this on a callout that was never initialized or already + * deinitialized. + * + * @param co Callout to deinitialize + */ +void bt_osal_callout_deinit(struct bt_osal_callout *co); + +/** + * @brief (Re)arm a callout to expire after the given delay + * + * Stops the callout if it is already running, then restarts it, so the delay is + * always measured from this call. + * + * @param co Callout to arm + * @param ticks Delay before expiry, in ticks + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_INVALID_PARAM or BT_OSAL_EINVAL if the underlying timer rejected the request + * - BT_OSAL_ERROR on any other failure + */ +bt_osal_error_t bt_osal_callout_reset(struct bt_osal_callout *co, + bt_osal_time_t ticks); + +/** + * @brief Stop a callout so it will not expire + * + * Does nothing if the callout is not running. If a previous expiry already + * posted an event to the callout's event queue, that event is removed from + * the queue as part of stopping (unless it has already been dequeued for processing). + * + * @param co Callout to stop + */ +void bt_osal_callout_stop(struct bt_osal_callout *co); + +/** + * @brief Check whether a callout is armed and has not expired yet + * + * @param co Callout to query + * + * @return true if the callout is running, false otherwise + */ +bool bt_osal_callout_is_active(struct bt_osal_callout *co); + +/** + * @brief Get the absolute time at which a callout will expire + * + * @param co Callout to query + * + * @return Expiry time in ticks, or 0 if it cannot be determined + */ +bt_osal_time_t bt_osal_callout_get_ticks(struct bt_osal_callout *co); + +/** + * @brief Get the time left before a callout expires + * + * @param co Callout to query + * @param time Reference time to measure from, in ticks (typically bt_osal_time_get()) + * + * @return Ticks remaining until expiry, or 0 if the callout has already expired + * at @p time or its expiry time cannot be determined + */ +bt_osal_time_t bt_osal_callout_remaining_ticks(struct bt_osal_callout *co, + bt_osal_time_t time); + +/** + * @brief Replace the argument passed to a callout's handler + * + * @param co Callout to update + * @param arg New argument to pass to the handler on expiry + */ +void bt_osal_callout_set_arg(struct bt_osal_callout *co, + void *arg); + +/** + * @brief Clear the queued flag of a callout's event + * + * Lets the callout be armed again after its event was dropped without being + * dispatched. It does not stop the timer. + * + * @param co Callout whose event should be reset + */ +void bt_osal_callout_mem_reset(struct bt_osal_callout *co); + +/* + * Time functions + */ + +/** + * @brief Get the current time + * + * @return Time since boot, in ticks + */ +bt_osal_time_t bt_osal_time_get(void); + +/** + * @brief Get the tick value that means "wait forever" + * + * Use it as the timeout of a pend/get to block indefinitely. Also available as + * the BT_OSAL_TIME_FOREVER macro. + * + * @return Tick value representing an infinite timeout + */ +bt_osal_time_t bt_osal_get_time_forever(void); + +/** + * @brief Convert milliseconds to ticks + * + * @param[in] ms Duration in milliseconds + * @param[out] out_ticks Resulting duration in ticks + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_EINVAL if the result does not fit in bt_osal_time_t + */ +bt_osal_error_t bt_osal_time_ms_to_ticks(uint32_t ms, bt_osal_time_t *out_ticks); + +/** + * @brief Convert ticks to milliseconds + * + * @param[in] ticks Duration in ticks + * @param[out] out_ms Resulting duration in milliseconds + * + * @return + * - BT_OSAL_OK on success + * - BT_OSAL_EINVAL if the result does not fit in uint32_t + */ +bt_osal_error_t bt_osal_time_ticks_to_ms(bt_osal_time_t ticks, uint32_t *out_ms); + +/** + * @brief Convert milliseconds to ticks, without overflow checking + * + * @param ms Duration in milliseconds + * + * @return Duration in ticks + */ +bt_osal_time_t bt_osal_time_ms_to_ticks32(uint32_t ms); + +/** + * @brief Convert ticks to milliseconds, without overflow checking + * + * @param ticks Duration in ticks + * + * @return Duration in milliseconds + */ +uint32_t bt_osal_time_ticks_to_ms32(bt_osal_time_t ticks); + +/** + * @brief Block the calling task for the given duration + * + * @note Must not be called from interrupt context. + * + * @param ticks Duration to block for, in ticks + */ +void bt_osal_time_delay(bt_osal_time_t ticks); + +/* + * Hardware-specific + * + * These symbols should be most likely defined by application since they are + * specific to hardware, not to OS. + */ + +#if NIMBLE_CFG_CONTROLLER + +/** + * @brief Install an interrupt handler for the given interrupt + * + * @param irqn Interrupt number to install the handler for + * @param addr Address of the interrupt handler + */ +void bt_osal_hw_set_isr(int irqn, uint32_t addr); + +#endif + +/** + * @brief Enter a critical section, disabling interrupts + * + * Critical sections nest: each call must be paired with a + * bt_osal_hw_exit_critical() call, and interrupts are only re-enabled by the + * outermost one. Keep the section as short as possible and never block in it. + * + * @return Context to be passed back to bt_osal_hw_exit_critical() + */ +uint32_t bt_osal_hw_enter_critical(void); + +/** + * @brief Leave a critical section entered with bt_osal_hw_enter_critical() + * + * @param ctx Context returned by the matching bt_osal_hw_enter_critical() call + */ +void bt_osal_hw_exit_critical(uint32_t ctx); + +/** + * @brief Check whether the caller is inside a critical section + * + * @return true if at least one critical section is currently entered, false otherwise + */ +bool bt_osal_hw_is_in_critical(void); + +#ifdef __cplusplus +} +#endif + +#endif /* _BT_OSAL_H_ */ diff --git a/components/bt/common/osal/include/bt_osal_freertos.h b/components/bt/common/osal/include/bt_osal_freertos.h new file mode 100644 index 00000000000..f10503a46b3 --- /dev/null +++ b/components/bt/common/osal/include/bt_osal_freertos.h @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: 2015-2022 The Apache Software Foundation (ASF) + * + * SPDX-License-Identifier: Apache-2.0 + * + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD + */ + +#ifndef _BT_OSAL_FREERTOS_H_ +#define _BT_OSAL_FREERTOS_H_ + +#ifdef __cplusplus +extern "C" { +#endif +#include "sdkconfig.h" + +/* Use esp timer instead of FreeRTOS timer to implement the callout. */ +#ifdef CONFIG_BT_OSAL_USE_ESP_TIMER +#define BT_OSAL_USE_ESP_TIMER (1) +#else +#define BT_OSAL_USE_ESP_TIMER (0) +#endif + +typedef void bt_osal_event_fn(struct bt_osal_event *ev); + +struct bt_osal_event_freertos { + bool queued; + bt_osal_event_fn *fn; + void *arg; + struct bt_osal_event *next; /*!< Next event in the queue's list, or NULL; valid only while @ref queued */ +}; + +struct bt_osal_eventq_freertos { + struct bt_osal_event *head; /*!< First event to dequeue, NULL when the queue is empty */ + struct bt_osal_event *tail; /*!< Last event in the list, NULL when the queue is empty */ + SemaphoreHandle_t sem; /*!< Binary "work pending" flag waking a blocked eventq_get(); the list is the source of truth, this only signals */ + portMUX_TYPE lock; /*!< Spinlock protecting head/tail and each event's next (task- and ISR-safe) */ + TaskHandle_t task; /*!< Handle of the task started to process this event queue, NULL if none */ + SemaphoreHandle_t done; /*!< Signalled by the worker just before it self-deletes, so eventq_deinit() can wait for a clean exit; NULL if no task */ + bool stop; /*!< Set by eventq_deinit() to ask the worker to exit at its next safe point (between events) */ +}; + +struct bt_osal_callout_freertos { +#if BT_OSAL_USE_ESP_TIMER + esp_timer_handle_t handle; +#else + TimerHandle_t handle; +#endif + struct bt_osal_eventq *evq; + struct bt_osal_event ev; +}; + +struct bt_osal_mutex_freertos { + SemaphoreHandle_t handle; +}; + +struct bt_osal_sem_freertos { + SemaphoreHandle_t handle; +}; + +typedef void bt_osal_event_fn_freertos(struct bt_osal_event_freertos *ev); + +// Eventq +struct bt_osal_event *bt_osal_freertos_eventq_get(struct bt_osal_eventq *evq, + bt_osal_time_t tmo); + +void bt_osal_freertos_eventq_put(struct bt_osal_eventq *evq, + struct bt_osal_event *ev); + +void bt_osal_freertos_eventq_put_to_front(struct bt_osal_eventq *evq, + struct bt_osal_event *ev); + +void bt_osal_freertos_eventq_remove(struct bt_osal_eventq *evq, + struct bt_osal_event *ev); + +bt_osal_error_t bt_osal_freertos_eventq_start(struct bt_osal_eventq *evq, + const struct bt_osal_task_info *info); + +bt_osal_error_t bt_osal_freertos_eventq_post_func(struct bt_osal_eventq *evq, + bt_osal_event_fn *fn, void *arg); + +// Mutex +bt_osal_error_t bt_osal_freertos_mutex_init(struct bt_osal_mutex *mu); +bt_osal_error_t bt_osal_freertos_mutex_deinit(struct bt_osal_mutex *mu); + +bt_osal_error_t bt_osal_freertos_mutex_pend(struct bt_osal_mutex *mu, + bt_osal_time_t timeout); + +bt_osal_error_t bt_osal_freertos_mutex_release(struct bt_osal_mutex *mu); + +// Semephore +bt_osal_error_t bt_osal_freertos_sem_init(struct bt_osal_sem *sem, uint16_t tokens); +bt_osal_error_t bt_osal_freertos_sem_deinit(struct bt_osal_sem *sem); + +bt_osal_error_t bt_osal_freertos_sem_pend(struct bt_osal_sem *sem, + bt_osal_time_t timeout); + +bt_osal_error_t bt_osal_freertos_sem_release(struct bt_osal_sem *sem); + +// Callout +bt_osal_error_t bt_osal_freertos_callout_init(struct bt_osal_callout *co, + struct bt_osal_eventq *evq, + bt_osal_event_fn *ev_cb, void *ev_arg); + +void bt_osal_freertos_callout_deinit(struct bt_osal_callout *co); + +void bt_osal_freertos_callout_stop(struct bt_osal_callout *co); + +bool bt_osal_freertos_callout_is_active(struct bt_osal_callout *co); + +bt_osal_time_t bt_osal_freertos_callout_get_ticks(struct bt_osal_callout *co); + +bt_osal_error_t bt_osal_freertos_callout_reset(struct bt_osal_callout *co, + bt_osal_time_t ticks); + +bt_osal_time_t bt_osal_freertos_callout_remaining_ticks(struct bt_osal_callout *co, + bt_osal_time_t now); + +// Time +bt_osal_error_t bt_osal_freertos_time_ms_to_ticks(uint32_t ms, + bt_osal_time_t *out_ticks); + +bt_osal_error_t bt_osal_freertos_time_ticks_to_ms(bt_osal_time_t ticks, + uint32_t *out_ms); + +// Hardware +uint32_t bt_osal_freertos_hw_enter_critical(void); + +void bt_osal_freertos_hw_exit_critical(uint32_t ctx); + +/** + * @brief Allocate and populate the OSAL function table + * + * Must be called before any bt_osal_* API is used; they all dispatch through the + * table. Asserts if the table cannot be allocated. + */ +void bt_osal_freertos_funcs_init(void); + +/** + * @brief Free the OSAL function table allocated by bt_osal_freertos_funcs_init() + */ +void bt_osal_freertos_funcs_deinit(void); + +/** + * @brief Get the OSAL function table + * + * @return Pointer to the table, or NULL if bt_osal_freertos_funcs_init() has not + * been called (or the table has since been deinitialized) + */ +struct bt_osal_funcs_t *bt_osal_freertos_funcs_get(void); + +#ifdef __cplusplus +} +#endif + +#endif /* _BT_OSAL_FREERTOS_H_ */ diff --git a/components/bt/common/osal/include/bt_osal_os.h b/components/bt/common/osal/include/bt_osal_os.h new file mode 100644 index 00000000000..7740bb63c9f --- /dev/null +++ b/components/bt/common/osal/include/bt_osal_os.h @@ -0,0 +1,441 @@ +/* + * SPDX-FileCopyrightText: 2015-2022 The Apache Software Foundation (ASF) + * + * SPDX-License-Identifier: Apache-2.0 + * + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD + */ + +#ifndef _BT_OSAL_OS_H_ +#define _BT_OSAL_OS_H_ + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#include "freertos/task.h" +#include "freertos/timers.h" +#include "esp_timer.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef ARRAY_SIZE +#define ARRAY_SIZE(array) \ + (sizeof(array) / sizeof((array)[0])) +#endif + +extern int ets_printf(const char *fmt, ...); +#define BT_OSAL_ASSERT(con) \ + do{ \ + if(!(con)) { \ + ets_printf("assertion:%s\n",#con); \ + ets_printf("line:%d,function:%s\n", __LINE__, __func__);\ + assert(0); \ + } \ + }while(0) + +#define BT_OSAL_OS_ALIGNMENT (4)/*bt_osal_get_os_alignment()*/ + +#define BT_OSAL_TIME_FOREVER bt_osal_get_time_forever() + +/* This should be compatible with TickType_t */ +typedef uint32_t bt_osal_time_t; +typedef int32_t bt_osal_stime_t; + +struct bt_osal_event; +typedef void bt_osal_event_fn(struct bt_osal_event *ev); + +struct bt_osal_event { + void *event; +}; + +struct bt_osal_eventq { + void *eventq; +}; + +struct bt_osal_callout { + void *co; +}; + +struct bt_osal_mutex { + void *mutex; +}; + +struct bt_osal_sem { + void *sem; +}; + +/** + * @brief Core affinity value meaning "let the scheduler pick any core" + */ +#define BT_OSAL_TASK_NO_AFFINITY ((BaseType_t)tskNO_AFFINITY) + +/** + * @brief Attributes for the task started to service an event queue + * + * Passed to bt_osal_eventq_start() so each queue's worker task can be given its + * own name, priority, stack and core affinity. + */ +struct bt_osal_task_info { + const char *name; /*!< Name of the task */ + uint8_t prio; /*!< FreeRTOS priority of the task */ + uint32_t stack_size; /*!< Stack depth of the task, in bytes */ + BaseType_t core_id; /*!< Core to pin the task to, or BT_OSAL_TASK_NO_AFFINITY to leave it unpinned */ +}; + +/* + * Simple APIs are just defined as static inline below, but some are a bit more + * complex or require some global state variables and thus are defined in .c + * file instead and static inline wrapper just calls proper implementation. + * We need declarations of these functions and they are defined in header below. + */ +#include "bt_osal_freertos.h" + +struct bt_osal_funcs_t { + bool (*p_bt_osal_os_started)(void); + void *(*p_bt_osal_get_current_task_id)(void); + void (*p_bt_osal_eventq_init)(struct bt_osal_eventq *); + void (*p_bt_osal_eventq_deinit)(struct bt_osal_eventq *); + struct bt_osal_event * (*p_bt_osal_eventq_get)(struct bt_osal_eventq *, bt_osal_time_t); + void (*p_bt_osal_eventq_put)(struct bt_osal_eventq *, struct bt_osal_event *); + void (*p_bt_osal_eventq_remove)(struct bt_osal_eventq *, struct bt_osal_event *); + void (*p_bt_osal_event_run)(struct bt_osal_event *); + bool (*p_bt_osal_eventq_is_empty)(struct bt_osal_eventq *); + void (*p_bt_osal_event_init)(struct bt_osal_event *, bt_osal_event_fn *, void *); + void (*p_bt_osal_event_deinit)(struct bt_osal_event *); + void (*p_bt_osal_event_reset)(struct bt_osal_event *); + bool (*p_bt_osal_event_is_queued)(struct bt_osal_event *); + void * (*p_bt_osal_event_get_arg)(struct bt_osal_event *); + void (*p_bt_osal_event_set_arg)(struct bt_osal_event *, void *); + bt_osal_error_t (*p_bt_osal_mutex_init)(struct bt_osal_mutex *); + bt_osal_error_t (*p_bt_osal_mutex_deinit)(struct bt_osal_mutex *); + bt_osal_error_t (*p_bt_osal_mutex_pend)(struct bt_osal_mutex *, bt_osal_time_t); + bt_osal_error_t (*p_bt_osal_mutex_release)(struct bt_osal_mutex *); + bt_osal_error_t (*p_bt_osal_sem_init)(struct bt_osal_sem *, uint16_t); + bt_osal_error_t (*p_bt_osal_sem_deinit)(struct bt_osal_sem *); + bt_osal_error_t (*p_bt_osal_sem_pend)(struct bt_osal_sem *, bt_osal_time_t); + bt_osal_error_t (*p_bt_osal_sem_release)(struct bt_osal_sem *); + uint16_t (*p_bt_osal_sem_get_count)(struct bt_osal_sem *); + bt_osal_error_t (*p_bt_osal_callout_init)(struct bt_osal_callout *, struct bt_osal_eventq *, bt_osal_event_fn *, void *); + bt_osal_error_t (*p_bt_osal_callout_reset)(struct bt_osal_callout *, bt_osal_time_t); + void (*p_bt_osal_callout_stop)(struct bt_osal_callout *); + void (*p_bt_osal_callout_deinit)(struct bt_osal_callout *); + void (*p_bt_osal_callout_mem_reset)(struct bt_osal_callout *); + bool (*p_bt_osal_callout_is_active)(struct bt_osal_callout *); + bt_osal_time_t (*p_bt_osal_callout_get_ticks)(struct bt_osal_callout *); + uint32_t (*p_bt_osal_callout_remaining_ticks)(struct bt_osal_callout *, bt_osal_time_t); + void (*p_bt_osal_callout_set_arg)(struct bt_osal_callout *, void *); + uint32_t (*p_bt_osal_time_get)(void); + bt_osal_error_t (*p_bt_osal_time_ms_to_ticks)(uint32_t ms, bt_osal_time_t *); + bt_osal_error_t (*p_bt_osal_time_ticks_to_ms)(bt_osal_time_t, uint32_t *); + bt_osal_time_t (*p_bt_osal_time_ms_to_ticks32)(uint32_t); + uint32_t (*p_bt_osal_time_ticks_to_ms32)(bt_osal_time_t); + void (*p_bt_osal_time_delay)(bt_osal_time_t); + void (*p_bt_osal_hw_set_isr)(int, uint32_t); + uint32_t (*p_bt_osal_hw_enter_critical)(void); + void (*p_bt_osal_hw_exit_critical)(uint32_t); + uint32_t (*p_bt_osal_get_time_forever)(void); + uint8_t (*p_bt_osal_hw_is_in_critical)(void); + void (*p_bt_osal_eventq_put_to_front)(struct bt_osal_eventq *, struct bt_osal_event *); + bt_osal_error_t (*p_bt_osal_eventq_start)(struct bt_osal_eventq *evq, const struct bt_osal_task_info *info); + bt_osal_error_t (*p_bt_osal_eventq_post_func)(struct bt_osal_eventq *evq, bt_osal_event_fn *fn, void *arg); +}; + +extern struct bt_osal_funcs_t *bt_osal_funcs; + +static inline bool +IRAM_ATTR bt_osal_os_started(void) +{ + return bt_osal_funcs->p_bt_osal_os_started(); +} + +static inline void * +IRAM_ATTR bt_osal_get_current_task_id(void) +{ + return bt_osal_funcs->p_bt_osal_get_current_task_id(); +} + +static inline void +IRAM_ATTR bt_osal_eventq_init(struct bt_osal_eventq *evq) +{ + return bt_osal_funcs->p_bt_osal_eventq_init(evq); +} + +static inline void +IRAM_ATTR bt_osal_eventq_deinit(struct bt_osal_eventq *evq) +{ + return bt_osal_funcs->p_bt_osal_eventq_deinit(evq); +} + +static inline struct bt_osal_event * +IRAM_ATTR bt_osal_eventq_get(struct bt_osal_eventq *evq, bt_osal_time_t tmo) +{ + return bt_osal_funcs->p_bt_osal_eventq_get(evq, tmo); +} + +static inline void +IRAM_ATTR bt_osal_eventq_put(struct bt_osal_eventq *evq, struct bt_osal_event *ev) +{ + return bt_osal_funcs->p_bt_osal_eventq_put(evq, ev); +} + +static inline void +IRAM_ATTR bt_osal_eventq_put_to_front(struct bt_osal_eventq *evq, struct bt_osal_event *ev) +{ + return bt_osal_funcs->p_bt_osal_eventq_put_to_front(evq, ev); +} + +static inline void +IRAM_ATTR bt_osal_eventq_remove(struct bt_osal_eventq *evq, struct bt_osal_event *ev) +{ + return bt_osal_funcs->p_bt_osal_eventq_remove(evq, ev); +} + +static inline void +IRAM_ATTR bt_osal_event_run(struct bt_osal_event *ev) +{ + return bt_osal_funcs->p_bt_osal_event_run(ev); +} + +static inline bool +IRAM_ATTR bt_osal_eventq_is_empty(struct bt_osal_eventq *evq) +{ + return bt_osal_funcs->p_bt_osal_eventq_is_empty(evq); +} + +static inline void +IRAM_ATTR bt_osal_event_init(struct bt_osal_event *ev, bt_osal_event_fn *fn, + void *arg) +{ + return bt_osal_funcs->p_bt_osal_event_init(ev, fn, arg); +} + +static inline bool +IRAM_ATTR bt_osal_event_is_queued(struct bt_osal_event *ev) +{ + return bt_osal_funcs->p_bt_osal_event_is_queued(ev); +} + +static inline void * +IRAM_ATTR bt_osal_event_get_arg(struct bt_osal_event *ev) +{ + return bt_osal_funcs->p_bt_osal_event_get_arg(ev); +} + +static inline void +IRAM_ATTR bt_osal_event_set_arg(struct bt_osal_event *ev, void *arg) +{ + return bt_osal_funcs->p_bt_osal_event_set_arg(ev, arg); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_mutex_init(struct bt_osal_mutex *mu) +{ + return bt_osal_funcs->p_bt_osal_mutex_init(mu); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_mutex_deinit(struct bt_osal_mutex *mu) +{ + return bt_osal_funcs->p_bt_osal_mutex_deinit(mu); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_mutex_pend(struct bt_osal_mutex *mu, bt_osal_time_t timeout) +{ + return bt_osal_funcs->p_bt_osal_mutex_pend(mu, timeout); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_mutex_release(struct bt_osal_mutex *mu) +{ + return bt_osal_funcs->p_bt_osal_mutex_release(mu); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_sem_init(struct bt_osal_sem *sem, uint16_t tokens) +{ + return bt_osal_funcs->p_bt_osal_sem_init(sem, tokens); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_sem_deinit(struct bt_osal_sem *sem) +{ + return bt_osal_funcs->p_bt_osal_sem_deinit(sem); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_sem_pend(struct bt_osal_sem *sem, bt_osal_time_t timeout) +{ + return bt_osal_funcs->p_bt_osal_sem_pend(sem, timeout); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_sem_release(struct bt_osal_sem *sem) +{ + return bt_osal_funcs->p_bt_osal_sem_release(sem); +} + +static inline uint16_t +IRAM_ATTR bt_osal_sem_get_count(struct bt_osal_sem *sem) +{ + return bt_osal_funcs->p_bt_osal_sem_get_count(sem); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_callout_init(struct bt_osal_callout *co, struct bt_osal_eventq *evq, + bt_osal_event_fn *ev_cb, void *ev_arg) +{ + return bt_osal_funcs->p_bt_osal_callout_init(co, evq, ev_cb, ev_arg); +} + +static inline void +IRAM_ATTR bt_osal_callout_deinit(struct bt_osal_callout *co) +{ + return bt_osal_funcs->p_bt_osal_callout_deinit(co); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_callout_reset(struct bt_osal_callout *co, bt_osal_time_t ticks) +{ + return bt_osal_funcs->p_bt_osal_callout_reset(co, ticks); +} + +static inline void +IRAM_ATTR bt_osal_callout_stop(struct bt_osal_callout *co) +{ + return bt_osal_funcs->p_bt_osal_callout_stop(co); +} + +static inline bool +IRAM_ATTR bt_osal_callout_is_active(struct bt_osal_callout *co) +{ + return bt_osal_funcs->p_bt_osal_callout_is_active(co); +} + +static inline bt_osal_time_t +IRAM_ATTR bt_osal_callout_get_ticks(struct bt_osal_callout *co) +{ + return bt_osal_funcs->p_bt_osal_callout_get_ticks(co); +} + +static inline bt_osal_time_t +IRAM_ATTR bt_osal_callout_remaining_ticks(struct bt_osal_callout *co, + bt_osal_time_t time) +{ + return bt_osal_funcs->p_bt_osal_callout_remaining_ticks(co, time); +} + +static inline void +IRAM_ATTR bt_osal_callout_set_arg(struct bt_osal_callout *co, void *arg) +{ + return bt_osal_funcs->p_bt_osal_callout_set_arg(co, arg); +} + +static inline bt_osal_time_t +IRAM_ATTR bt_osal_time_get(void) +{ + return bt_osal_funcs->p_bt_osal_time_get(); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_time_ms_to_ticks(uint32_t ms, bt_osal_time_t *out_ticks) +{ + return bt_osal_funcs->p_bt_osal_time_ms_to_ticks(ms, out_ticks); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_time_ticks_to_ms(bt_osal_time_t ticks, uint32_t *out_ms) +{ + return bt_osal_funcs->p_bt_osal_time_ticks_to_ms(ticks, out_ms); +} + +static inline bt_osal_time_t +IRAM_ATTR bt_osal_time_ms_to_ticks32(uint32_t ms) +{ + return bt_osal_funcs->p_bt_osal_time_ms_to_ticks32(ms); +} + +static inline uint32_t +IRAM_ATTR bt_osal_time_ticks_to_ms32(bt_osal_time_t ticks) +{ + return bt_osal_funcs->p_bt_osal_time_ticks_to_ms32(ticks); +} + +static inline void +IRAM_ATTR bt_osal_time_delay(bt_osal_time_t ticks) +{ + return bt_osal_funcs->p_bt_osal_time_delay(ticks); +} + +#if NIMBLE_CFG_CONTROLLER +static inline void +IRAM_ATTR bt_osal_hw_set_isr(int irqn, uint32_t addr) +{ + return bt_osal_funcs->p_bt_osal_hw_set_isr(irqn, addr); +} +#endif + +static inline uint32_t +IRAM_ATTR bt_osal_hw_enter_critical(void) +{ + return bt_osal_funcs->p_bt_osal_hw_enter_critical(); +} + +static inline void +IRAM_ATTR bt_osal_hw_exit_critical(uint32_t ctx) +{ + return bt_osal_funcs->p_bt_osal_hw_exit_critical(ctx); +} + +static inline bool +IRAM_ATTR bt_osal_hw_is_in_critical(void) +{ + return bt_osal_funcs->p_bt_osal_hw_is_in_critical(); +} + +static inline bt_osal_time_t +IRAM_ATTR bt_osal_get_time_forever(void) +{ + return bt_osal_funcs->p_bt_osal_get_time_forever(); +} + +static inline void +IRAM_ATTR bt_osal_callout_mem_reset(struct bt_osal_callout *co) +{ + return bt_osal_funcs->p_bt_osal_callout_mem_reset(co); +} + +static inline void +IRAM_ATTR bt_osal_event_deinit(struct bt_osal_event *ev) +{ + return bt_osal_funcs->p_bt_osal_event_deinit(ev); +} + +static inline void +IRAM_ATTR bt_osal_event_reset(struct bt_osal_event *ev) +{ + return bt_osal_funcs->p_bt_osal_event_reset(ev); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_eventq_start(struct bt_osal_eventq *evq, const struct bt_osal_task_info *info) +{ + return bt_osal_funcs->p_bt_osal_eventq_start(evq, info); +} + +static inline bt_osal_error_t +IRAM_ATTR bt_osal_eventq_post_func(struct bt_osal_eventq *evq, bt_osal_event_fn *fn, void *arg) +{ + return bt_osal_funcs->p_bt_osal_eventq_post_func(evq, fn, arg); +} + +#ifdef __cplusplus +} +#endif + +#endif /* _BT_OSAL_OS_H_ */ diff --git a/components/bt/common/osal/src/bt_osal_freertos.c b/components/bt/common/osal/src/bt_osal_freertos.c new file mode 100644 index 00000000000..c8ed8139d93 --- /dev/null +++ b/components/bt/common/osal/src/bt_osal_freertos.c @@ -0,0 +1,1409 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "bt_osal.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#include "freertos/task.h" +#include "freertos/timers.h" +#include "freertos/portable.h" +#include "bt_osal_freertos.h" + +#include "esp_log.h" +#include "esp_heap_caps.h" +#include "soc/soc_caps.h" + +static portMUX_TYPE s_bt_osal_port_mutex = portMUX_INITIALIZER_UNLOCKED; +static uint8_t s_bt_osal_in_critical[portNUM_PROCESSORS]; + +#if BT_OSAL_USE_ESP_TIMER +static const char *TAG = "Timer"; +#endif + +/* + * One-shot work item posted with bt_osal_freertos_eventq_post_func(). The embedded + * event (which must stay the first member so it can be cast back to the item) + * is queued to the eventq; its backing storage is embedded too so a single + * allocation/free covers the whole item. + */ +struct bt_osal_freertos_work_item { + struct bt_osal_event ev; /*!< Event queued to the eventq (must be first) */ + struct bt_osal_event_freertos storage; /*!< Backing storage for @ref ev */ + bt_osal_event_fn *fn; /*!< User event handler to run */ +}; + +static void bt_osal_freertos_work_item_run(struct bt_osal_event *ev); + +/* + * All OSAL objects (events, event queues, callouts, mutexes, semaphores) are + * touched from ISR context, so their backing storage must come from internal + * RAM: an object placed in PSRAM is unreachable while the flash cache is + * disabled. Allocate it here rather than through bt_osi_mem, which lives in the + * NimBLE controller porting layer and is not built on every target/host combo. + */ +static void *bt_osal_malloc(size_t size) +{ + return heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); +} + +static void bt_osal_free(void *ptr) +{ + if (ptr) { + heap_caps_free(ptr); + } +} + +void bt_osal_freertos_event_run(struct bt_osal_event *ev); + +/* + * Free @p ev's backing work item iff it is an OSAL-owned one-shot item. Returns + * true if reclaimed, false for the stop sentinel and caller-owned events (which + * are left untouched). Safe to call with a NULL/uninitialized event. + */ +static bool bt_osal_freertos_reclaim_work_item(struct bt_osal_event *ev) +{ + struct bt_osal_event_freertos *event; + + if (ev == NULL || ev->event == NULL) { + return false; + } + event = (struct bt_osal_event_freertos *)ev->event; + if (event->fn != bt_osal_freertos_work_item_run) { + return false; + } + /* ev is the first member of the work item, so it aliases the item base. */ + bt_osal_free((struct bt_osal_freertos_work_item *)ev); + return true; +} + +bool IRAM_ATTR bt_osal_freertos_os_started(void) +{ + return xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED; +} + +void * IRAM_ATTR bt_osal_freertos_get_current_task_id(void) +{ + return xTaskGetCurrentTaskHandle(); +} + +void IRAM_ATTR bt_osal_freertos_event_init(struct bt_osal_event *ev, bt_osal_event_fn *fn, + void *arg) +{ + struct bt_osal_event_freertos *event = NULL; + if(!ev->event) { + ev->event = bt_osal_malloc(sizeof(struct bt_osal_event_freertos)); + } + event = (struct bt_osal_event_freertos *)ev->event; + BT_OSAL_ASSERT(event); + + memset(event, 0, sizeof(*event)); + event->fn = fn; + event->arg = arg; +} + +void IRAM_ATTR bt_osal_freertos_event_deinit(struct bt_osal_event *ev) +{ + if (!ev->event) { + return; + } + bt_osal_free(ev->event); + ev->event = NULL; +} + +void IRAM_ATTR bt_osal_freertos_event_reset(struct bt_osal_event *ev) +{ + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)ev->event; + BT_OSAL_ASSERT(event); + // Resetting a still-queued event is a caller error + BT_OSAL_ASSERT(!event->queued); + event->queued = 0; +} + +/* + * Intrusive singly-linked list helpers for the event queue. The list threads + * through each event's backing storage (struct bt_osal_event_freertos::next), + * so no extra allocation is needed. Every helper below (except the flush) assumes + * the caller already holds eventq->lock (entered via portENTER_CRITICAL[_ISR]). + */ +static struct bt_osal_event_freertos *IRAM_ATTR bt_osal_event_priv(struct bt_osal_event *ev) +{ + return (struct bt_osal_event_freertos *)ev->event; +} + +/* Append @p ev to the tail of the list. */ +static void IRAM_ATTR bt_osal_list_put_tail(struct bt_osal_eventq_freertos *eventq, + struct bt_osal_event *ev) +{ + bt_osal_event_priv(ev)->next = NULL; + if (eventq->tail != NULL) { + bt_osal_event_priv(eventq->tail)->next = ev; + } else { + eventq->head = ev; + } + eventq->tail = ev; +} + +/* Insert @p ev at the head of the list. */ +static void IRAM_ATTR bt_osal_list_put_head(struct bt_osal_eventq_freertos *eventq, + struct bt_osal_event *ev) +{ + bt_osal_event_priv(ev)->next = eventq->head; + eventq->head = ev; + if (eventq->tail == NULL) { + eventq->tail = ev; + } +} + +/* Pop and return the head event, or NULL if the list is empty. */ +static struct bt_osal_event *IRAM_ATTR bt_osal_list_pop_head(struct bt_osal_eventq_freertos *eventq) +{ + struct bt_osal_event *ev = eventq->head; + + if (ev != NULL) { + struct bt_osal_event_freertos *event = bt_osal_event_priv(ev); + eventq->head = event->next; + if (eventq->head == NULL) { + eventq->tail = NULL; + } + event->next = NULL; + event->queued = false; + } + + return ev; +} + +/* Unlink @p ev from the list if present. Caller clears ev's queued flag. */ +static void IRAM_ATTR bt_osal_list_unlink(struct bt_osal_eventq_freertos *eventq, + struct bt_osal_event *ev) +{ + struct bt_osal_event *cur = eventq->head; + struct bt_osal_event *prev = NULL; + + while (cur != NULL) { + struct bt_osal_event_freertos *cur_priv = bt_osal_event_priv(cur); + + if (cur == ev) { + if (prev != NULL) { + bt_osal_event_priv(prev)->next = cur_priv->next; + } else { + eventq->head = cur_priv->next; + } + if (cur_priv->next == NULL) { + eventq->tail = prev; + } + cur_priv->next = NULL; + return; + } + prev = cur; + cur = cur_priv->next; + } +} + +/* + * Detach every event and clear the wakeup semaphore so the queue matches a + * freshly initialized (empty) state. Task context only (takes the semaphore). + * + * OSAL-owned one-shot items (post_func) can only be freed on dispatch or by + * reclaim; once detached here they can never be dispatched, so they must be + * reclaimed to avoid a heap leak. Caller-owned events are left untouched + * except for having their queued/next flags cleared so they stay re-postable. + */ +static void bt_osal_freertos_eventq_flush(struct bt_osal_eventq_freertos *eventq) +{ + struct bt_osal_event *ev; + + /* Detach the whole list under the lock, then process it outside. */ + portENTER_CRITICAL(&eventq->lock); + ev = eventq->head; + eventq->head = NULL; + eventq->tail = NULL; + portEXIT_CRITICAL(&eventq->lock); + + while (ev != NULL) { + struct bt_osal_event_freertos *event = bt_osal_event_priv(ev); + /* Save next first: reclaim may free the node backing this event. */ + struct bt_osal_event *next = event->next; + event->next = NULL; + event->queued = false; + /* Frees OSAL-owned one-shot items; no-op for caller-owned events. */ + bt_osal_freertos_reclaim_work_item(ev); + ev = next; + } + + /* Binary semaphore: a single take clears any pending wakeup. */ + xSemaphoreTake(eventq->sem, 0); +} + +void bt_osal_freertos_eventq_init(struct bt_osal_eventq *evq) +{ + struct bt_osal_eventq_freertos *eventq = NULL; + if(!evq->eventq) { + evq->eventq = bt_osal_malloc(sizeof(struct bt_osal_eventq_freertos)); + eventq = (struct bt_osal_eventq_freertos*)evq->eventq; + BT_OSAL_ASSERT(eventq); + memset(eventq, 0, sizeof(*eventq)); + eventq->head = NULL; + eventq->tail = NULL; + eventq->lock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED; + /* + * Binary "work pending" flag, not a per-event token count. The list is + * the single source of truth for what is queued. + */ + eventq->sem = xSemaphoreCreateBinary(); + BT_OSAL_ASSERT(eventq->sem); + } else { + eventq = (struct bt_osal_eventq_freertos*)evq->eventq; + bt_osal_freertos_eventq_flush(eventq); + } +} + +/* + * Ask the event queue's worker task to exit, and block until it has. Kept in one + * place so the stop sequence has a single home (and is ready to expose as a + * public stop API should the need ever arise). No-op if no worker was started. + * + * vTaskDelete() on the worker would strike it at an arbitrary point (possibly + * mid-callback, holding a mutex or in a critical section). Instead we ask it to + * exit itself: set `stop`, then post a sentinel event to the front of the queue + * to wake it out of eventq_get(). Front-posting guarantees the worker's next + * dequeue is the sentinel, so no still-queued event is popped and dropped on the + * way out; whatever remains is reclaimed by the teardown sweep in + * bt_osal_freertos_eventq_deinit(). The worker observes `stop` at a safe point + * between events, signals `done` and self-deletes; we wait on `done` here. + * + * Caller must have validated that evq->eventq is non-NULL. + */ +static void bt_osal_freertos_eventq_stop_worker(struct bt_osal_eventq *evq) +{ + struct bt_osal_eventq_freertos *eventq = (struct bt_osal_eventq_freertos *)evq->eventq; + struct bt_osal_event stop_ev = {0}; + + if (eventq->task == NULL) { + return; + } + + eventq->stop = true; + bt_osal_freertos_event_init(&stop_ev, NULL, NULL); + /* Front-posted so the worker's next dequeue is the sentinel (see above). */ + bt_osal_freertos_eventq_put_to_front(evq, &stop_ev); + + xSemaphoreTake(eventq->done, portMAX_DELAY); + + /* Worker has exited and no longer touches the queue or the sentinel. */ + bt_osal_freertos_eventq_remove(evq, &stop_ev); + bt_osal_freertos_event_deinit(&stop_ev); + vSemaphoreDelete(eventq->done); + eventq->done = NULL; + eventq->task = NULL; +} + +/* + * Reclaim OSAL-owned one-shot work items still queued at teardown so they do not + * leak; caller-owned events are simply dropped (the caller frees their storage). + * + * Surviving caller-owned events have their queued/next flags cleared so they + * uphold the module-wide "detached => queued=false" invariant (mirroring + * pop_head, eventq_remove and flush). This keeps such an event re-postable and + * lets the documented bt_osal_event_reset / bt_osal_callout_mem_reset recovery + * path run without tripping its BT_OSAL_ASSERT(!event->queued). + */ +static void bt_osal_freertos_eventq_reclaim_pending(struct bt_osal_eventq_freertos *eventq) +{ + struct bt_osal_event *ev = eventq->head; + + while (ev != NULL) { + struct bt_osal_event_freertos *event = bt_osal_event_priv(ev); + /* Save next first: reclaim may free the node backing this event. */ + struct bt_osal_event *next = event->next; + /* Frees OSAL-owned one-shot items; returns false for caller-owned. */ + if (!bt_osal_freertos_reclaim_work_item(ev)) { + event->next = NULL; + event->queued = false; + } + ev = next; + } + eventq->head = NULL; + eventq->tail = NULL; +} + +void bt_osal_freertos_eventq_deinit(struct bt_osal_eventq *evq) +{ + struct bt_osal_eventq_freertos *eventq = (struct bt_osal_eventq_freertos *)evq->eventq; + + /* Deinit can be invoked twice without init . Handle this case */ + if (eventq == NULL) { + return; + } + BT_OSAL_ASSERT(eventq); + + /* Gracefully stop the worker (if any) before deleting the queue it waits on. */ + bt_osal_freertos_eventq_stop_worker(evq); + + /* Worker is gone: reclaim any OSAL-owned one-shot items left queued. */ + bt_osal_freertos_eventq_reclaim_pending(eventq); + + vSemaphoreDelete(eventq->sem); + bt_osal_free((void *)eventq); + evq->eventq = NULL; +} + +void IRAM_ATTR bt_osal_freertos_callout_mem_reset(struct bt_osal_callout *co) +{ + struct bt_osal_callout_freertos *callout = (struct bt_osal_callout_freertos *)co->co; + + BT_OSAL_ASSERT(callout); + BT_OSAL_ASSERT(callout->handle); + + bt_osal_event_reset(&callout->ev); +} + +static inline bool IRAM_ATTR in_isr(void) +{ + /* XXX hw specific! */ + return xPortInIsrContext() != 0; +} + +struct bt_osal_event *IRAM_ATTR bt_osal_freertos_eventq_get(struct bt_osal_eventq *evq, bt_osal_time_t tmo) +{ + struct bt_osal_event *ev = NULL; + struct bt_osal_eventq_freertos *eventq = (struct bt_osal_eventq_freertos *)evq->eventq; + if (in_isr()) { + BT_OSAL_ASSERT(tmo == 0); + /* + * The list is the source of truth, so pop directly. Leaving the wakeup + * semaphore untouched only ever over-signals a blocked consumer (safe); + * taking it here could instead clear a signal owned by another + * still-queued event and strand it. + */ + portENTER_CRITICAL_ISR(&eventq->lock); + ev = bt_osal_list_pop_head(eventq); + portEXIT_CRITICAL_ISR(&eventq->lock); + return ev; + } + + /* + * Pop first; only block on the wakeup semaphore when the list is empty. + * A binary semaphore coalesces signals, so a token count cannot mirror the + * list length; instead put() links then gives, and here we pop then take. + * That ordering never loses a wakeup, while a coalesced (extra) signal or a + * concurrent eventq_remove() emptying the list merely costs one spurious + * re-check before we block again. + * + * A finite timeout must stay an upper bound across those retries, so the + * remaining budget is recomputed from the entry tick before waiting again. + * An infinite wait needs no bookkeeping and skips the tick read entirely. + */ + TickType_t start_tick = (tmo != portMAX_DELAY) ? xTaskGetTickCount() : 0; + bt_osal_time_t remaining = tmo; + + while (1) { + portENTER_CRITICAL(&eventq->lock); + ev = bt_osal_list_pop_head(eventq); + portEXIT_CRITICAL(&eventq->lock); + + if (ev != NULL) { + return ev; + } + + if (tmo != portMAX_DELAY) { + /* Modular arithmetic keeps this correct across tick counter wrap. */ + TickType_t elapsed = xTaskGetTickCount() - start_tick; + if (elapsed >= tmo) { + return NULL; + } + remaining = tmo - elapsed; + } + + if (xSemaphoreTake(eventq->sem, remaining) != pdTRUE) { + return NULL; + } + } +} + +void IRAM_ATTR bt_osal_freertos_eventq_put(struct bt_osal_eventq *evq, struct bt_osal_event *ev) +{ + struct bt_osal_eventq_freertos *eventq = (struct bt_osal_eventq_freertos *)evq->eventq; + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)ev->event; + BaseType_t woken; + + if (in_isr()) { + portENTER_CRITICAL_ISR(&eventq->lock); + if (event->queued) { + portEXIT_CRITICAL_ISR(&eventq->lock); + return; + } + event->queued = true; + bt_osal_list_put_tail(eventq, ev); + portEXIT_CRITICAL_ISR(&eventq->lock); + + woken = pdFALSE; + /* Wake a blocked consumer. The semaphore is a binary "work pending" + * flag, so a give that finds it already set fails harmlessly (a wakeup + * is already pending) — the linked event is the source of truth. */ + xSemaphoreGiveFromISR(eventq->sem, &woken); + if (woken == pdTRUE) { + portYIELD_FROM_ISR(); + } + } else { + portENTER_CRITICAL(&eventq->lock); + if (event->queued) { + portEXIT_CRITICAL(&eventq->lock); + return; + } + event->queued = true; + bt_osal_list_put_tail(eventq, ev); + portEXIT_CRITICAL(&eventq->lock); + + /* Signal availability outside the spinlock (FreeRTOS API is not + * callable inside a critical section). Binary semaphore: a give that + * finds it already set fails harmlessly, so the result is ignored. */ + (void)xSemaphoreGive(eventq->sem); + } +} + +void IRAM_ATTR bt_osal_freertos_eventq_put_to_front(struct bt_osal_eventq *evq, struct bt_osal_event *ev) +{ + struct bt_osal_eventq_freertos *eventq = (struct bt_osal_eventq_freertos *)evq->eventq; + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)ev->event; + BaseType_t woken; + + if (in_isr()) { + portENTER_CRITICAL_ISR(&eventq->lock); + if (event->queued) { + portEXIT_CRITICAL_ISR(&eventq->lock); + return; + } + event->queued = true; + bt_osal_list_put_head(eventq, ev); + portEXIT_CRITICAL_ISR(&eventq->lock); + + woken = pdFALSE; + /* Best-effort wakeup; see bt_osal_freertos_eventq_put(). */ + xSemaphoreGiveFromISR(eventq->sem, &woken); + if (woken == pdTRUE) { + portYIELD_FROM_ISR(); + } + } else { + portENTER_CRITICAL(&eventq->lock); + if (event->queued) { + portEXIT_CRITICAL(&eventq->lock); + return; + } + event->queued = true; + bt_osal_list_put_head(eventq, ev); + portEXIT_CRITICAL(&eventq->lock); + + /* Best-effort wakeup; see bt_osal_freertos_eventq_put(). */ + (void)xSemaphoreGive(eventq->sem); + } +} + +void IRAM_ATTR bt_osal_freertos_eventq_remove(struct bt_osal_eventq *evq, + struct bt_osal_event *ev) +{ + struct bt_osal_eventq_freertos *eventq = (struct bt_osal_eventq_freertos *)evq->eventq; + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)ev->event; + /* + * Unlink from the list only; leave the wakeup semaphore alone. The list is + * the source of truth, so a now-stale "work pending" signal just makes a + * blocked consumer re-check and find nothing (safe over-signal). Taking the + * semaphore here could instead clear a signal owned by another still-queued + * event and strand it. + */ + if (in_isr()) { + portENTER_CRITICAL_ISR(&eventq->lock); + if (event->queued) { + bt_osal_list_unlink(eventq, ev); + event->queued = 0; + } + portEXIT_CRITICAL_ISR(&eventq->lock); + } else { + portENTER_CRITICAL(&eventq->lock); + if (event->queued) { + bt_osal_list_unlink(eventq, ev); + event->queued = 0; + } + portEXIT_CRITICAL(&eventq->lock); + } +} + +bool IRAM_ATTR bt_osal_freertos_eventq_is_empty(struct bt_osal_eventq *evq) +{ + struct bt_osal_eventq_freertos *eventq = (struct bt_osal_eventq_freertos *)evq->eventq; + bool empty; + + if (in_isr()) { + portENTER_CRITICAL_ISR(&eventq->lock); + empty = (eventq->head == NULL); + portEXIT_CRITICAL_ISR(&eventq->lock); + } else { + portENTER_CRITICAL(&eventq->lock); + empty = (eventq->head == NULL); + portEXIT_CRITICAL(&eventq->lock); + } + + return empty; +} + +static void bt_osal_freertos_eventq_task(void *arg) +{ + struct bt_osal_eventq *evq = (struct bt_osal_eventq *)arg; + struct bt_osal_eventq_freertos *eventq = (struct bt_osal_eventq_freertos *)evq->eventq; + struct bt_osal_event *ev; + + while (1) { + /* Block forever until an event is posted, then dispatch it */ + ev = bt_osal_freertos_eventq_get(evq, portMAX_DELAY); + + /* Graceful stop: stop_worker() set `stop` and woke us with a sentinel + * front-posted to the queue, so `ev` is normally that sentinel. Acknowledge + * and self-delete here, at a safe point between events, so the task is + * never torn down mid-callback. If a race dequeued a real item instead, + * it is dropped unrun; reclaim it when the OSAL owns it (the sentinel and + * caller-owned events are left alone) so it does not leak. */ + if (eventq->stop) { + bt_osal_freertos_reclaim_work_item(ev); + xSemaphoreGive(eventq->done); + vTaskDelete(NULL); + } + + if (ev != NULL) { + bt_osal_freertos_event_run(ev); + } + } +} + +bt_osal_error_t bt_osal_freertos_eventq_start(struct bt_osal_eventq *evq, + const struct bt_osal_task_info *info) +{ + struct bt_osal_eventq_freertos *eventq; + BaseType_t ret; + + if (evq == NULL || evq->eventq == NULL || info == NULL || info->name == NULL) { + return BT_OSAL_EINVAL; + } + + /* xTaskCreatePinnedToCore() asserts on a bad core, so reject it here and let + * the caller handle it instead of aborting the system. */ + if (info->core_id != BT_OSAL_TASK_NO_AFFINITY && + (info->core_id < 0 || info->core_id >= portNUM_PROCESSORS)) { + return BT_OSAL_EINVAL; + } + + eventq = (struct bt_osal_eventq_freertos *)evq->eventq; + + /* A task is already processing this queue */ + if (eventq->task != NULL) { + return BT_OSAL_EINVAL; + } + + /* Completion signal the worker raises when it exits; deinit waits on it. */ + eventq->stop = false; + eventq->done = xSemaphoreCreateBinary(); + if (eventq->done == NULL) { + return BT_OSAL_ENOMEM; + } + + ret = xTaskCreatePinnedToCore(bt_osal_freertos_eventq_task, info->name, + info->stack_size, evq, info->prio, + &eventq->task, info->core_id); + if (ret != pdPASS) { + vSemaphoreDelete(eventq->done); + eventq->done = NULL; + eventq->task = NULL; + return BT_OSAL_ENOMEM; + } + + return BT_OSAL_OK; +} + +static void bt_osal_freertos_work_item_run(struct bt_osal_event *ev) +{ + /* ev is the first member of the item, so it aliases the item itself. */ + struct bt_osal_freertos_work_item *item = (struct bt_osal_freertos_work_item *)ev; + + /* Run the user handler, then release the one-shot work item. */ + item->fn(ev); + bt_osal_free(item); +} + +bt_osal_error_t bt_osal_freertos_eventq_post_func(struct bt_osal_eventq *evq, + bt_osal_event_fn *fn, void *arg) +{ + struct bt_osal_freertos_work_item *item; + + if (evq == NULL || evq->eventq == NULL || fn == NULL) { + return BT_OSAL_EINVAL; + } + + /* Memory allocation is not ISR-safe; reject calls from interrupt context. */ + if (in_isr()) { + return BT_OSAL_ERR_IN_ISR; + } + + item = bt_osal_malloc(sizeof(*item)); + if (item == NULL) { + return BT_OSAL_ENOMEM; + } + + item->fn = fn; + + /* + * Point the event at the embedded storage so bt_osal_freertos_event_init() + * skips its internal allocation. The wrapper handler receives the event + * and forwards it to the user handler, whose argument (@p arg) is + * retrievable via bt_osal_event_get_arg(). + */ + item->ev.event = &item->storage; + bt_osal_freertos_event_init(&item->ev, bt_osal_freertos_work_item_run, arg); + + bt_osal_freertos_eventq_put(evq, &item->ev); + + return BT_OSAL_OK; +} + +bt_osal_error_t bt_osal_freertos_mutex_init(struct bt_osal_mutex *mu) +{ + struct bt_osal_mutex_freertos *mutex = NULL; + + if(!mu->mutex) { + mu->mutex = bt_osal_malloc(sizeof(struct bt_osal_mutex_freertos)); + mutex = (struct bt_osal_mutex_freertos *)mu->mutex; + + if (!mutex) { + return BT_OSAL_ENOMEM; + } + + memset(mutex, 0, sizeof(*mutex)); + mutex->handle = xSemaphoreCreateRecursiveMutex(); + if (!mutex->handle) { + bt_osal_free((void *)mutex); + mu->mutex = NULL; + return BT_OSAL_ENOMEM; + } + } + + return BT_OSAL_OK; +} + +bt_osal_error_t bt_osal_freertos_mutex_deinit(struct bt_osal_mutex *mu) +{ + struct bt_osal_mutex_freertos *mutex = (struct bt_osal_mutex_freertos *)mu->mutex; + + if (!mutex) { + return BT_OSAL_INVALID_PARAM; + } + + BT_OSAL_ASSERT(mutex->handle); + vSemaphoreDelete(mutex->handle); + + bt_osal_free((void *)mutex); + mu->mutex = NULL; + + return BT_OSAL_OK; +} + +void IRAM_ATTR bt_osal_freertos_event_run(struct bt_osal_event *ev) +{ + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)ev->event; + event->fn(ev); +} + +bool IRAM_ATTR bt_osal_freertos_event_is_queued(struct bt_osal_event *ev) +{ + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)ev->event; + return event->queued; +} + +void * IRAM_ATTR bt_osal_freertos_event_get_arg(struct bt_osal_event *ev) +{ + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)ev->event; + return event->arg; +} + +void IRAM_ATTR bt_osal_freertos_event_set_arg(struct bt_osal_event *ev, void *arg) +{ + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)ev->event; + event->arg = arg; +} + + +bt_osal_error_t IRAM_ATTR bt_osal_freertos_mutex_pend(struct bt_osal_mutex *mu, bt_osal_time_t timeout) +{ + BaseType_t ret; + struct bt_osal_mutex_freertos *mutex = (struct bt_osal_mutex_freertos *)mu->mutex; + + if (!mutex) { + return BT_OSAL_INVALID_PARAM; + } + + BT_OSAL_ASSERT(mutex->handle); + + if (in_isr()) { + ret = pdFAIL; + BT_OSAL_ASSERT(0); + } else { + ret = xSemaphoreTakeRecursive(mutex->handle, timeout); + } + + return ret == pdPASS ? BT_OSAL_OK : BT_OSAL_TIMEOUT; +} + +bt_osal_error_t IRAM_ATTR bt_osal_freertos_mutex_release(struct bt_osal_mutex *mu) +{ + struct bt_osal_mutex_freertos *mutex = (struct bt_osal_mutex_freertos *)mu->mutex; + + if (!mutex) { + return BT_OSAL_INVALID_PARAM; + } + + BT_OSAL_ASSERT(mutex->handle); + + if (in_isr()) { + BT_OSAL_ASSERT(0); + } else { + if (xSemaphoreGiveRecursive(mutex->handle) != pdPASS) { + return BT_OSAL_BAD_MUTEX; + } + } + + return BT_OSAL_OK; +} + +bt_osal_error_t bt_osal_freertos_sem_init(struct bt_osal_sem *sem, uint16_t tokens) +{ + struct bt_osal_sem_freertos *semaphore = NULL; + + if(!sem->sem) { + sem->sem = bt_osal_malloc(sizeof(struct bt_osal_sem_freertos)); + semaphore = (struct bt_osal_sem_freertos *)sem->sem; + + if (!semaphore) { + return BT_OSAL_ENOMEM; + } + + memset(semaphore, 0, sizeof(*semaphore)); + semaphore->handle = xSemaphoreCreateCounting(128, tokens); + if (!semaphore->handle) { + bt_osal_free((void *)semaphore); + sem->sem = NULL; + return BT_OSAL_ENOMEM; + } + } + + return BT_OSAL_OK; +} + +bt_osal_error_t bt_osal_freertos_sem_deinit(struct bt_osal_sem *sem) +{ + struct bt_osal_sem_freertos *semaphore = (struct bt_osal_sem_freertos *)sem->sem; + + if (!semaphore) { + return BT_OSAL_INVALID_PARAM; + } + + BT_OSAL_ASSERT(semaphore->handle); + vSemaphoreDelete(semaphore->handle); + + bt_osal_free((void *)semaphore); + sem->sem = NULL; + + return BT_OSAL_OK; +} + +bt_osal_error_t IRAM_ATTR bt_osal_freertos_sem_pend(struct bt_osal_sem *sem, bt_osal_time_t timeout) +{ + BaseType_t woken = pdFALSE; + BaseType_t ret; + struct bt_osal_sem_freertos *semaphore = (struct bt_osal_sem_freertos *)sem->sem; + + if (!semaphore) { + return BT_OSAL_INVALID_PARAM; + } + + BT_OSAL_ASSERT(semaphore->handle); + + if (in_isr()) { + BT_OSAL_ASSERT(timeout == 0); + ret = xSemaphoreTakeFromISR(semaphore->handle, &woken); + if( woken == pdTRUE ) { + portYIELD_FROM_ISR(); + } + } else { + ret = xSemaphoreTake(semaphore->handle, timeout); + } + + return ret == pdPASS ? BT_OSAL_OK : BT_OSAL_TIMEOUT; +} + +bt_osal_error_t IRAM_ATTR bt_osal_freertos_sem_release(struct bt_osal_sem *sem) +{ + BaseType_t ret; + BaseType_t woken = pdFALSE; + struct bt_osal_sem_freertos *semaphore = (struct bt_osal_sem_freertos *)sem->sem; + + if (!semaphore) { + return BT_OSAL_INVALID_PARAM; + } + + BT_OSAL_ASSERT(semaphore->handle); + + if (in_isr()) { + ret = xSemaphoreGiveFromISR(semaphore->handle, &woken); + if( woken == pdTRUE ) { + portYIELD_FROM_ISR(); + } + } else { + ret = xSemaphoreGive(semaphore->handle); + } + + /* A give fails only when the counting semaphore is already at its maximum + * count; report it as a runtime error instead of silently swallowing it in + * assertions-disabled builds. */ + if (ret != pdPASS) { + return BT_OSAL_ERROR; + } + + return BT_OSAL_OK; +} + +#if BT_OSAL_USE_ESP_TIMER +static void IRAM_ATTR bt_osal_event_fn_wrapper(void *arg) +{ + struct bt_osal_callout_freertos *callout = (struct bt_osal_callout_freertos *)arg; + BT_OSAL_ASSERT(callout); + + if (callout->evq) { + bt_osal_eventq_put(callout->evq, &callout->ev); + } else { + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)callout->ev.event; + event->fn(&callout->ev); + } +} + +static IRAM_ATTR bt_osal_error_t esp_err_to_bt_osal_error(esp_err_t err) +{ + switch(err) { + case ESP_ERR_INVALID_ARG: + return BT_OSAL_INVALID_PARAM; + + case ESP_ERR_INVALID_STATE: + return BT_OSAL_EINVAL; + + case ESP_OK: + return BT_OSAL_OK; + + default: + return BT_OSAL_ERROR; + } +} +#else + +static void IRAM_ATTR bt_osal_callout_timer_cb(TimerHandle_t timer) +{ + struct bt_osal_callout_freertos *callout; + + callout = pvTimerGetTimerID(timer); + BT_OSAL_ASSERT(callout); + + if (callout->evq) { + bt_osal_eventq_put(callout->evq, &callout->ev); + } else { + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)callout->ev.event; + event->fn(&callout->ev); + } +} +#endif + +bt_osal_error_t bt_osal_freertos_callout_init(struct bt_osal_callout *co, struct bt_osal_eventq *evq, + bt_osal_event_fn *ev_cb, void *ev_arg) +{ + struct bt_osal_callout_freertos *callout = NULL; + + if(!co->co) { + co->co = bt_osal_malloc(sizeof(struct bt_osal_callout_freertos)); + callout = (struct bt_osal_callout_freertos *)co->co; + if (!callout) { + return BT_OSAL_ENOMEM; + } + + memset(callout, 0, sizeof(*callout)); + bt_osal_event_init(&callout->ev, ev_cb, ev_arg); + /* Set for both timer backends: with a NULL evq the expiry callback runs + * the event inline in timer context instead of queueing it. */ + callout->evq = evq; + +#if BT_OSAL_USE_ESP_TIMER + esp_timer_create_args_t create_args = { + .callback = bt_osal_event_fn_wrapper, + .arg = callout, + .name = "bt_osal_co" + }; + + if (esp_timer_create(&create_args, &callout->handle) != ESP_OK) { + bt_osal_event_deinit(&callout->ev); + bt_osal_free((void *)callout); + co->co = NULL; + return BT_OSAL_ENOMEM; + } +#else + callout->handle = xTimerCreate("co", 1, pdFALSE, callout, bt_osal_callout_timer_cb); + + if (!callout->handle) { + bt_osal_event_deinit(&callout->ev); + bt_osal_free((void *)callout); + co->co = NULL; + return BT_OSAL_ENOMEM; + } +#endif // BT_OSAL_USE_ESP_TIMER + } + else { + callout = (struct bt_osal_callout_freertos *)co->co; + BT_OSAL_ASSERT(callout); + callout->evq = evq; + bt_osal_event_init(&callout->ev, ev_cb, ev_arg); + } + + return BT_OSAL_OK; +} + +void bt_osal_freertos_callout_deinit(struct bt_osal_callout *co) +{ + struct bt_osal_callout_freertos *callout = (struct bt_osal_callout_freertos *)co->co; + + /* Since we dynamically deinit timers, function can be called for NULL timers. Return for such scenarios */ + if (!callout) { + return; + } + + if (!callout->handle) { + return; + } + + /* Stop and delete the timer BEFORE deinitializing the event. Otherwise a + * timer callback firing on the esp_timer/timer-daemon task could dereference + * callout->ev.event after it has been freed and set to NULL. */ +#if BT_OSAL_USE_ESP_TIMER + esp_err_t err = esp_timer_stop(callout->handle); + if(err != ESP_OK) { + if (err != ESP_ERR_INVALID_STATE) { // ESP_ERR_INVALID_STATE is expected when timer is already stopped + ESP_LOGD(TAG, "Timer not stopped"); + } + } + err = esp_timer_delete(callout->handle); + if(err != ESP_OK) { + ESP_LOGW(TAG, "Timer not deleted"); + } +#else + xTimerStop(callout->handle, portMAX_DELAY); + xTimerDelete(callout->handle, portMAX_DELAY); +#endif // BT_OSAL_USE_ESP_TIMER + + /* Unlink the event from the queue's intrusive list before freeing its + * backing memory. A one-shot timer that already fired may have posted + * &callout->ev to callout->evq, leaving a dangling pointer otherwise. */ + if (callout->evq) { + bt_osal_freertos_eventq_remove(callout->evq, &callout->ev); + } + + bt_osal_event_deinit(&callout->ev); + + bt_osal_free((void *)callout); + co->co = NULL; + memset(co, 0, sizeof(struct bt_osal_callout)); +} + +uint16_t IRAM_ATTR bt_osal_freertos_sem_get_count(struct bt_osal_sem *sem) +{ + struct bt_osal_sem_freertos *semaphore = (struct bt_osal_sem_freertos *)sem->sem; + + if (!semaphore) { + return 0; + } + + return uxSemaphoreGetCount(semaphore->handle); +} + + +bt_osal_error_t IRAM_ATTR bt_osal_freertos_callout_reset(struct bt_osal_callout *co, bt_osal_time_t ticks) +{ + struct bt_osal_callout_freertos *callout = (struct bt_osal_callout_freertos *)co->co; +#if BT_OSAL_USE_ESP_TIMER + esp_timer_stop(callout->handle); + if (callout->evq) { + bt_osal_freertos_eventq_remove(callout->evq, &callout->ev); + } + + return esp_err_to_bt_osal_error(esp_timer_start_once(callout->handle, (uint64_t)ticks * 1000)); +#else + + BaseType_t woken1 = pdFALSE, woken2 = pdFALSE, woken3 = pdFALSE; + + if (ticks == 0) { + ticks = 1; + } + if (in_isr()) { + xTimerStopFromISR(callout->handle, &woken1); + if (callout->evq) { + bt_osal_freertos_eventq_remove(callout->evq, &callout->ev); + } + xTimerChangePeriodFromISR(callout->handle, ticks, &woken2); + xTimerResetFromISR(callout->handle, &woken3); + + if( woken1 == pdTRUE || woken2 == pdTRUE || woken3 == pdTRUE) { + portYIELD_FROM_ISR(); + } + } else { + xTimerStop(callout->handle, portMAX_DELAY); + if (callout->evq) { + bt_osal_freertos_eventq_remove(callout->evq, &callout->ev); + } + xTimerChangePeriod(callout->handle, ticks, portMAX_DELAY); + xTimerReset(callout->handle, portMAX_DELAY); + } + + return BT_OSAL_OK; +#endif +} + +void IRAM_ATTR bt_osal_freertos_callout_stop(struct bt_osal_callout *co) +{ + struct bt_osal_callout_freertos *callout = (struct bt_osal_callout_freertos *)co->co; + + if (!callout) { + return; + } + +#if BT_OSAL_USE_ESP_TIMER + /* esp_timer_stop is ISR-safe (uses portENTER_CRITICAL_SAFE internally), + * so call it unconditionally to guarantee the timer is disarmed. */ + esp_timer_stop(callout->handle); +#else + if (in_isr()) { + BaseType_t woken = pdFALSE; + xTimerStopFromISR(callout->handle, &woken); + if (woken == pdTRUE) { + portYIELD_FROM_ISR(); + } + } else { + xTimerStop(callout->handle, portMAX_DELAY); + } +#endif + + if (callout->evq) { + bt_osal_freertos_eventq_remove(callout->evq, &callout->ev); + } +} + +bool IRAM_ATTR bt_osal_freertos_callout_is_active(struct bt_osal_callout *co) +{ + struct bt_osal_callout_freertos *callout = (struct bt_osal_callout_freertos *)co->co; + if (!callout) { + return false; + } +#if BT_OSAL_USE_ESP_TIMER + return esp_timer_is_active(callout->handle); +#else + return xTimerIsTimerActive(callout->handle) == pdTRUE; +#endif +} + +bt_osal_time_t IRAM_ATTR bt_osal_freertos_callout_get_ticks(struct bt_osal_callout *co) +{ +#if BT_OSAL_USE_ESP_TIMER + + uint32_t exp = 0; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + uint64_t expiry = 0; + esp_err_t err; + + struct bt_osal_callout_freertos *callout = (struct bt_osal_callout_freertos *)co->co; + + //Fetch expiry time in microseconds + err = esp_timer_get_expiry_time((esp_timer_handle_t)(callout->handle), &expiry); + if (err != ESP_OK) { + //Error. Could not fetch the expiry time + return 0; + } + + //Convert microseconds to ticks + bt_osal_freertos_time_ms_to_ticks((uint32_t)(expiry / 1000), &exp); +#else + //esp_timer_get_expiry_time() is only available from IDF 5.0 onwards + /* Returning 0 from here should not cause any effect. + * Drawback of this approach is that existing code to reset timer would be called + * more often (since the if condition to invoke reset timer would always succeed if + * timer is active). + */ + exp = 0; +#endif //ESP_IDF_VERSION + + return exp; +#else + struct bt_osal_callout_freertos *callout = (struct bt_osal_callout_freertos *)co->co; + return xTimerGetExpiryTime(callout->handle); +#endif +} + +bt_osal_time_t IRAM_ATTR bt_osal_freertos_callout_remaining_ticks(struct bt_osal_callout *co, + bt_osal_time_t now) +{ + bt_osal_time_t rt; + uint32_t exp = 0; + + struct bt_osal_callout_freertos *callout = (struct bt_osal_callout_freertos *)co->co; + +#if BT_OSAL_USE_ESP_TIMER +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + uint64_t expiry = 0; + esp_err_t err; + + //Fetch expiry time in microseconds + err = esp_timer_get_expiry_time((esp_timer_handle_t)(callout->handle), &expiry); + if (err != ESP_OK) { + //Error. Could not fetch the expiry time + return 0; + } + + //Convert microseconds to ticks + bt_osal_freertos_time_ms_to_ticks((uint32_t)(expiry / 1000), &exp); +#else + //esp_timer_get_expiry_time() is only available from IDF 5.0 onwards + //Set expiry to 0 + exp = 0; +#endif //ESP_IDF_VERSION +#else + exp = xTimerGetExpiryTime(callout->handle); +#endif + + //Use signed modular subtraction so the comparison stays correct even when + //the 32-bit tick/millisecond counter wraps around (valid for periods up to + //half the uint32_t range). + if ((bt_osal_stime_t)(exp - now) > 0) { + rt = exp - now; + } else { + rt = 0; + } + + return rt; +} + +void IRAM_ATTR bt_osal_freertos_callout_set_arg(struct bt_osal_callout *co, void *arg) +{ + struct bt_osal_callout_freertos *callout = (struct bt_osal_callout_freertos *)co->co; + struct bt_osal_event_freertos *event = (struct bt_osal_event_freertos *)callout->ev.event; + event->arg = arg; +} + +uint32_t IRAM_ATTR bt_osal_freertos_time_get(void) +{ +#if BT_OSAL_USE_ESP_TIMER + return esp_timer_get_time() / 1000; +#else + return xTaskGetTickCountFromISR(); +#endif +} + +bt_osal_error_t IRAM_ATTR bt_osal_freertos_time_ms_to_ticks(uint32_t ms, bt_osal_time_t *out_ticks) +{ + uint64_t ticks; +#if BT_OSAL_USE_ESP_TIMER + ticks = (uint64_t)ms; +#else + ticks = ((uint64_t)ms * configTICK_RATE_HZ) / 1000; +#endif + if (ticks > UINT32_MAX) { + return BT_OSAL_EINVAL; + } + + *out_ticks = ticks; + + return 0; +} + +bt_osal_error_t IRAM_ATTR bt_osal_freertos_time_ticks_to_ms(bt_osal_time_t ticks, uint32_t *out_ms) +{ + uint64_t ms; +#if BT_OSAL_USE_ESP_TIMER + ms = ((uint64_t)ticks); +#else + ms = ((uint64_t)ticks * 1000) / configTICK_RATE_HZ; +#endif + if (ms > UINT32_MAX) { + return BT_OSAL_EINVAL; + } + + *out_ms = ms; + + return 0; +} + +bt_osal_time_t IRAM_ATTR bt_osal_freertos_time_ms_to_ticks32(uint32_t ms) +{ +#if BT_OSAL_USE_ESP_TIMER + return ms; +#else + return (bt_osal_time_t)((uint64_t)ms * configTICK_RATE_HZ / 1000); +#endif +} + +uint32_t IRAM_ATTR bt_osal_freertos_time_ticks_to_ms32(bt_osal_time_t ticks) +{ +#if BT_OSAL_USE_ESP_TIMER + return ticks; +#else + return (uint32_t)((uint64_t)ticks * 1000 / configTICK_RATE_HZ); +#endif +} + +void IRAM_ATTR bt_osal_freertos_time_delay(bt_osal_time_t ticks) +{ +#if BT_OSAL_USE_ESP_TIMER + vTaskDelay(ticks / portTICK_PERIOD_MS); +#else + vTaskDelay(ticks); +#endif +} + +uint32_t IRAM_ATTR bt_osal_freertos_hw_enter_critical(void) +{ + BaseType_t core; + + portENTER_CRITICAL(&s_bt_osal_port_mutex); + core = xPortGetCoreID(); + if (core < portNUM_PROCESSORS) { + ++s_bt_osal_in_critical[core]; + } + return 0; +} + +uint8_t IRAM_ATTR bt_osal_freertos_hw_is_in_critical(void) +{ + BaseType_t core; + + core = xPortGetCoreID(); + if (core >= portNUM_PROCESSORS) { + return 0; + } + return s_bt_osal_in_critical[core]; +} + +void IRAM_ATTR bt_osal_freertos_hw_exit_critical(uint32_t ctx) +{ + BaseType_t core; + + core = xPortGetCoreID(); + if (core < portNUM_PROCESSORS && s_bt_osal_in_critical[core] > 0) { + --s_bt_osal_in_critical[core]; + } + portEXIT_CRITICAL(&s_bt_osal_port_mutex); + +} + +uint32_t IRAM_ATTR bt_osal_freertos_get_time_forever(void) +{ + return portMAX_DELAY; +} + +const struct bt_osal_funcs_t bt_osal_funcs_ro = { + .p_bt_osal_os_started = bt_osal_freertos_os_started, + .p_bt_osal_get_current_task_id = bt_osal_freertos_get_current_task_id, + .p_bt_osal_eventq_init = bt_osal_freertos_eventq_init, + .p_bt_osal_eventq_deinit = bt_osal_freertos_eventq_deinit, + .p_bt_osal_eventq_get = bt_osal_freertos_eventq_get, + .p_bt_osal_eventq_put = bt_osal_freertos_eventq_put, + .p_bt_osal_eventq_put_to_front = bt_osal_freertos_eventq_put_to_front, + .p_bt_osal_eventq_remove = bt_osal_freertos_eventq_remove, + .p_bt_osal_event_run = bt_osal_freertos_event_run, + .p_bt_osal_eventq_is_empty = bt_osal_freertos_eventq_is_empty, + .p_bt_osal_event_init = bt_osal_freertos_event_init, + .p_bt_osal_event_deinit = bt_osal_freertos_event_deinit, + .p_bt_osal_event_reset = bt_osal_freertos_event_reset, + .p_bt_osal_event_is_queued = bt_osal_freertos_event_is_queued, + .p_bt_osal_event_get_arg = bt_osal_freertos_event_get_arg, + .p_bt_osal_event_set_arg = bt_osal_freertos_event_set_arg, + .p_bt_osal_mutex_init = bt_osal_freertos_mutex_init, + .p_bt_osal_mutex_deinit = bt_osal_freertos_mutex_deinit, + .p_bt_osal_mutex_pend = bt_osal_freertos_mutex_pend, + .p_bt_osal_mutex_release = bt_osal_freertos_mutex_release, + .p_bt_osal_sem_init = bt_osal_freertos_sem_init, + .p_bt_osal_sem_deinit = bt_osal_freertos_sem_deinit, + .p_bt_osal_sem_pend = bt_osal_freertos_sem_pend, + .p_bt_osal_sem_release = bt_osal_freertos_sem_release, + .p_bt_osal_sem_get_count = bt_osal_freertos_sem_get_count, + .p_bt_osal_callout_init = bt_osal_freertos_callout_init, + .p_bt_osal_callout_reset = bt_osal_freertos_callout_reset, + .p_bt_osal_callout_stop = bt_osal_freertos_callout_stop, + .p_bt_osal_callout_deinit = bt_osal_freertos_callout_deinit, + .p_bt_osal_callout_mem_reset = bt_osal_freertos_callout_mem_reset, + .p_bt_osal_callout_is_active = bt_osal_freertos_callout_is_active, + .p_bt_osal_callout_get_ticks = bt_osal_freertos_callout_get_ticks, + .p_bt_osal_callout_remaining_ticks = bt_osal_freertos_callout_remaining_ticks, + .p_bt_osal_callout_set_arg = bt_osal_freertos_callout_set_arg, + .p_bt_osal_time_get = bt_osal_freertos_time_get, + .p_bt_osal_time_ms_to_ticks = bt_osal_freertos_time_ms_to_ticks, + .p_bt_osal_time_ticks_to_ms = bt_osal_freertos_time_ticks_to_ms, + .p_bt_osal_time_ms_to_ticks32 = bt_osal_freertos_time_ms_to_ticks32, + .p_bt_osal_time_ticks_to_ms32 = bt_osal_freertos_time_ticks_to_ms32, + .p_bt_osal_time_delay = bt_osal_freertos_time_delay, +#if NIMBLE_CFG_CONTROLLER || CONFIG_NIMBLE_CONTROLLER_MODE + .p_bt_osal_hw_set_isr = NULL, +#endif + .p_bt_osal_hw_enter_critical = bt_osal_freertos_hw_enter_critical, + .p_bt_osal_hw_exit_critical = bt_osal_freertos_hw_exit_critical, + .p_bt_osal_get_time_forever = bt_osal_freertos_get_time_forever, + .p_bt_osal_hw_is_in_critical = bt_osal_freertos_hw_is_in_critical, + // Added + .p_bt_osal_eventq_start = bt_osal_freertos_eventq_start, + .p_bt_osal_eventq_post_func = bt_osal_freertos_eventq_post_func +}; + +struct bt_osal_funcs_t *bt_osal_funcs = NULL; + +struct bt_osal_funcs_t * bt_osal_freertos_funcs_get(void) +{ + return bt_osal_funcs; +} + +void bt_osal_freertos_funcs_init(void) +{ + bt_osal_funcs = (struct bt_osal_funcs_t *)bt_osal_malloc(sizeof(struct bt_osal_funcs_t)); + if(!bt_osal_funcs) { + printf("OSAL funcs init failed\n"); + assert(0); + return; + } + memcpy(bt_osal_funcs, &bt_osal_funcs_ro, sizeof(struct bt_osal_funcs_t)); +} + +void bt_osal_freertos_funcs_deinit(void) +{ + if (bt_osal_funcs) { + bt_osal_free(bt_osal_funcs); + } + bt_osal_funcs = NULL; +} + +void bt_osal_func_init(void) +{ + return bt_osal_freertos_funcs_init(); +} + +void bt_osal_func_deinit(void) +{ + return bt_osal_freertos_funcs_deinit(); +} + +struct bt_osal_funcs_t* bt_osal_funcs_get(void) +{ + return bt_osal_freertos_funcs_get(); +} diff --git a/components/bt/host/bluedroid/api/esp_bt_main.c b/components/bt/host/bluedroid/api/esp_bt_main.c index 5bab29fe830..a5db84c1c8b 100644 --- a/components/bt/host/bluedroid/api/esp_bt_main.c +++ b/components/bt/host/bluedroid/api/esp_bt_main.c @@ -17,6 +17,8 @@ #include "config/stack_config.h" #include "hci_log/bt_hci_log.h" #include "bt_common.h" +#include "bt_osal.h" +#include "bt_prf_task.h" static esp_bluedroid_status_t s_bt_host_state = ESP_BLUEDROID_STATUS_UNINITIALIZED; @@ -218,6 +220,17 @@ esp_err_t esp_bluedroid_init_with_cfg(esp_bluedroid_config_t *cfg) } #endif // (BT_HCI_LOG_INCLUDED == TRUE) + /* Bring up the host-agnostic bt_osal function table */ + bt_osal_freertos_funcs_init(); + +#if CONFIG_BT_PRF_TASK_ENABLED + /* Start the shared BLE profile task now that the bt_osal table is up, so + * profiles can post work without spawning their own tasks. */ + if (bt_prf_task_init() != BT_OSAL_OK) { + LOG_WARN("bt_prf_task_init failed"); + } +#endif + s_bt_host_state = ESP_BLUEDROID_STATUS_INITIALIZED; return ESP_OK; @@ -274,6 +287,15 @@ esp_err_t esp_bluedroid_deinit(void) osi_mem_deinit(); #endif +#if CONFIG_BT_PRF_TASK_ENABLED + /* Stop the shared BLE profile task while the bt_osal table is still up, + * symmetrically with the init in esp_bluedroid_init_with_cfg(). */ + bt_prf_task_deinit(); +#endif + + /* Release the bt_osal function table set up in esp_bluedroid_init_with_cfg(). */ + bt_osal_freertos_funcs_deinit(); + s_bt_host_state = ESP_BLUEDROID_STATUS_UNINITIALIZED; return ESP_OK; } diff --git a/components/bt/host/nimble/nimble b/components/bt/host/nimble/nimble index 843673ada2e..5311c952a53 160000 --- a/components/bt/host/nimble/nimble +++ b/components/bt/host/nimble/nimble @@ -1 +1 @@ -Subproject commit 843673ada2e33301376edc373f96b17460244fc1 +Subproject commit 5311c952a537a609347725b90059ae43fcf9021d diff --git a/components/bt/test_apps/basic_unit_test/main/CMakeLists.txt b/components/bt/test_apps/basic_unit_test/main/CMakeLists.txt index d873380d2bc..0c7c86ead9a 100644 --- a/components/bt/test_apps/basic_unit_test/main/CMakeLists.txt +++ b/components/bt/test_apps/basic_unit_test/main/CMakeLists.txt @@ -1,6 +1,8 @@ idf_component_register(SRCS "test_bt_main.c" "test_bt_common.c" "test_smp.c" + "test_osal.c" + "test_prf_task.c" INCLUDE_DIRS "." PRIV_REQUIRES unity bt WHOLE_ARCHIVE) diff --git a/components/bt/test_apps/basic_unit_test/main/test_osal.c b/components/bt/test_apps/basic_unit_test/main/test_osal.c new file mode 100644 index 00000000000..3cd3fca3d16 --- /dev/null +++ b/components/bt/test_apps/basic_unit_test/main/test_osal.c @@ -0,0 +1,674 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* + * Unit tests for the BT OSAL porting layer (components/bt/common/osal): events and + * the list-backed event queue, the queue worker task, mutexes, semaphores, + * callouts, time helpers and critical sections. + * + * Every test brings the OSAL function table up and tears it down again, so the + * per-test heap check in tearDown() also covers the layer's own allocations. + */ + +#include +#include + +#include "unity.h" +#include "unity_test_runner.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" + +#include "bt_osal.h" +#include "bt_osal_freertos.h" + +#define TEST_EVQ_STACK_SIZE 3072 +#define TEST_EVQ_PRIO (configMAX_PRIORITIES - 3) +#define TEST_HELPER_STACK 2560 + +/* Long enough for a blocking call to succeed, short enough to keep the suite quick. */ +#define TEST_WAIT_MS 1000 +/* Waited on when an operation is expected NOT to happen. */ +#define TEST_NO_EVENT_MS 50 + +/* Shared state between the test task and the event/timer callbacks. */ +static SemaphoreHandle_t s_done; +static volatile uint32_t s_run_count; +static struct bt_osal_event *s_last_ev; +static void *s_last_arg; + +/* Attributes shared by the worker tasks started across these tests. */ +static const struct bt_osal_task_info s_test_task_info = { + .name = "osal_test_evq", + .prio = TEST_EVQ_PRIO, + .stack_size = TEST_EVQ_STACK_SIZE, + .core_id = BT_OSAL_TASK_NO_AFFINITY, +}; + +/* Core the worker task dispatched the last event on; recorded by ev_cb_core(). */ +static volatile BaseType_t s_last_core; + +static void ev_cb_count(struct bt_osal_event *ev) +{ + s_run_count++; + s_last_ev = ev; + s_last_arg = bt_osal_event_get_arg(ev); + xSemaphoreGive(s_done); +} + +static void ev_cb_core(struct bt_osal_event *ev) +{ + s_last_core = xPortGetCoreID(); + ev_cb_count(ev); +} + +/* Bring up the OSAL function table; every bt_osal_* call dispatches through it. */ +static void osal_test_begin(void) +{ + s_run_count = 0; + s_last_ev = NULL; + s_last_arg = NULL; + + s_done = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_done); + + bt_osal_freertos_funcs_init(); + TEST_ASSERT_NOT_NULL(bt_osal_freertos_funcs_get()); +} + +static void osal_test_end(void) +{ + bt_osal_freertos_funcs_deinit(); + TEST_ASSERT_NULL(bt_osal_freertos_funcs_get()); + + vSemaphoreDelete(s_done); + s_done = NULL; + + /* Tasks deleted during the test (queue workers, helpers) and timers deleted + * by callout_deinit() are freed asynchronously by the idle and timer tasks; + * give them a moment so the heap check in tearDown() is not tripped. + */ + vTaskDelay(pdMS_TO_TICKS(20)); +} + +TEST_CASE("osal event init, run, arg accessors and deinit", "[osal]") +{ + struct bt_osal_event ev = {0}; + + osal_test_begin(); + + bt_osal_event_init(&ev, ev_cb_count, (void *)0x1234); + TEST_ASSERT_NOT_NULL(ev.event); + TEST_ASSERT_FALSE(bt_osal_event_is_queued(&ev)); + TEST_ASSERT_EQUAL_PTR((void *)0x1234, bt_osal_event_get_arg(&ev)); + + bt_osal_event_set_arg(&ev, (void *)0x5678); + TEST_ASSERT_EQUAL_PTR((void *)0x5678, bt_osal_event_get_arg(&ev)); + + /* Running an event outside a queue calls the handler in the caller's context. */ + bt_osal_event_run(&ev); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + TEST_ASSERT_EQUAL_PTR(&ev, s_last_ev); + TEST_ASSERT_EQUAL_PTR((void *)0x5678, s_last_arg); + + bt_osal_event_deinit(&ev); + TEST_ASSERT_NULL(ev.event); + + /* Deinit of an already released event is a no-op. */ + bt_osal_event_deinit(&ev); + + osal_test_end(); +} + +TEST_CASE("osal eventq keeps FIFO order and ignores a duplicate put", "[osal]") +{ + struct bt_osal_eventq evq = {0}; + struct bt_osal_event ev1 = {0}; + struct bt_osal_event ev2 = {0}; + struct bt_osal_event ev3 = {0}; + + osal_test_begin(); + + bt_osal_eventq_init(&evq); + TEST_ASSERT_NOT_NULL(evq.eventq); + TEST_ASSERT_TRUE(bt_osal_eventq_is_empty(&evq)); + TEST_ASSERT_NULL(bt_osal_eventq_get(&evq, 0)); + + bt_osal_event_init(&ev1, ev_cb_count, NULL); + bt_osal_event_init(&ev2, ev_cb_count, NULL); + bt_osal_event_init(&ev3, ev_cb_count, NULL); + + bt_osal_eventq_put(&evq, &ev1); + bt_osal_eventq_put(&evq, &ev2); + /* An event that is already queued must not be linked (or counted) twice. */ + bt_osal_eventq_put(&evq, &ev1); + TEST_ASSERT_TRUE(bt_osal_event_is_queued(&ev1)); + TEST_ASSERT_FALSE(bt_osal_eventq_is_empty(&evq)); + + /* put_to_front() jumps the queue. */ + bt_osal_eventq_put_to_front(&evq, &ev3); + + TEST_ASSERT_EQUAL_PTR(&ev3, bt_osal_eventq_get(&evq, 0)); + TEST_ASSERT_EQUAL_PTR(&ev1, bt_osal_eventq_get(&evq, 0)); + TEST_ASSERT_EQUAL_PTR(&ev2, bt_osal_eventq_get(&evq, 0)); + + TEST_ASSERT_TRUE(bt_osal_eventq_is_empty(&evq)); + TEST_ASSERT_FALSE(bt_osal_event_is_queued(&ev1)); + /* The duplicate put left no extra token behind: the queue is really empty. */ + TEST_ASSERT_NULL(bt_osal_eventq_get(&evq, pdMS_TO_TICKS(TEST_NO_EVENT_MS))); + + bt_osal_event_deinit(&ev1); + bt_osal_event_deinit(&ev2); + bt_osal_event_deinit(&ev3); + bt_osal_eventq_deinit(&evq); + TEST_ASSERT_NULL(evq.eventq); + + osal_test_end(); +} + +TEST_CASE("osal eventq_remove unlinks head, middle and tail events", "[osal]") +{ + struct bt_osal_eventq evq = {0}; + struct bt_osal_event ev1 = {0}; + struct bt_osal_event ev2 = {0}; + struct bt_osal_event ev3 = {0}; + + osal_test_begin(); + + bt_osal_eventq_init(&evq); + bt_osal_event_init(&ev1, ev_cb_count, NULL); + bt_osal_event_init(&ev2, ev_cb_count, NULL); + bt_osal_event_init(&ev3, ev_cb_count, NULL); + + /* Remove from the middle. */ + bt_osal_eventq_put(&evq, &ev1); + bt_osal_eventq_put(&evq, &ev2); + bt_osal_eventq_put(&evq, &ev3); + bt_osal_eventq_remove(&evq, &ev2); + TEST_ASSERT_FALSE(bt_osal_event_is_queued(&ev2)); + TEST_ASSERT_EQUAL_PTR(&ev1, bt_osal_eventq_get(&evq, 0)); + TEST_ASSERT_EQUAL_PTR(&ev3, bt_osal_eventq_get(&evq, 0)); + TEST_ASSERT_TRUE(bt_osal_eventq_is_empty(&evq)); + /* The removed event's token was reclaimed, so a get() must block and time out. */ + TEST_ASSERT_NULL(bt_osal_eventq_get(&evq, pdMS_TO_TICKS(TEST_NO_EVENT_MS))); + + /* Remove the head. */ + bt_osal_eventq_put(&evq, &ev1); + bt_osal_eventq_put(&evq, &ev2); + bt_osal_eventq_remove(&evq, &ev1); + TEST_ASSERT_EQUAL_PTR(&ev2, bt_osal_eventq_get(&evq, 0)); + TEST_ASSERT_NULL(bt_osal_eventq_get(&evq, pdMS_TO_TICKS(TEST_NO_EVENT_MS))); + + /* Remove the tail. */ + bt_osal_eventq_put(&evq, &ev1); + bt_osal_eventq_put(&evq, &ev2); + bt_osal_eventq_remove(&evq, &ev2); + TEST_ASSERT_EQUAL_PTR(&ev1, bt_osal_eventq_get(&evq, 0)); + TEST_ASSERT_NULL(bt_osal_eventq_get(&evq, pdMS_TO_TICKS(TEST_NO_EVENT_MS))); + + /* Removing an event that is not queued is a no-op. */ + bt_osal_eventq_remove(&evq, &ev3); + TEST_ASSERT_TRUE(bt_osal_eventq_is_empty(&evq)); + + /* The queue is reusable after all that shuffling. */ + bt_osal_eventq_put(&evq, &ev3); + TEST_ASSERT_EQUAL_PTR(&ev3, bt_osal_eventq_get(&evq, 0)); + + bt_osal_event_deinit(&ev1); + bt_osal_event_deinit(&ev2); + bt_osal_event_deinit(&ev3); + bt_osal_eventq_deinit(&evq); + + osal_test_end(); +} + +TEST_CASE("osal eventq worker task dispatches queued events", "[osal]") +{ + struct bt_osal_eventq evq = {0}; + struct bt_osal_eventq not_inited = {0}; + struct bt_osal_event ev = {0}; + + osal_test_begin(); + + /* A queue that was never initialized has nothing to service. */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, + bt_osal_eventq_start(¬_inited, &s_test_task_info)); + + bt_osal_eventq_init(&evq); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, + bt_osal_eventq_start(&evq, &s_test_task_info)); + /* Only one task may service a queue. */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, + bt_osal_eventq_start(&evq, &s_test_task_info)); + + bt_osal_event_init(&ev, ev_cb_count, (void *)0xC0FFEE); + bt_osal_eventq_put(&evq, &ev); + + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + TEST_ASSERT_EQUAL_PTR(&ev, s_last_ev); + TEST_ASSERT_EQUAL_PTR((void *)0xC0FFEE, s_last_arg); + /* The worker cleared the queued flag before running the handler. */ + TEST_ASSERT_FALSE(bt_osal_event_is_queued(&ev)); + + bt_osal_event_deinit(&ev); + /* deinit() stops the worker task before releasing the queue it waits on. */ + bt_osal_eventq_deinit(&evq); + + osal_test_end(); +} + +TEST_CASE("osal eventq worker honours the requested core affinity", "[osal]") +{ + struct bt_osal_eventq evq = {0}; + struct bt_osal_event ev = {0}; + struct bt_osal_task_info info = s_test_task_info; + + osal_test_begin(); + + bt_osal_eventq_init(&evq); + + /* A core that does not exist is rejected instead of asserting inside FreeRTOS. */ + info.core_id = portNUM_PROCESSORS; + TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, bt_osal_eventq_start(&evq, &info)); + info.core_id = -2; + TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, bt_osal_eventq_start(&evq, &info)); + + /* Pinned to CPU0, the worker must dispatch from CPU0. */ + info.core_id = 0; + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_eventq_start(&evq, &info)); + + s_last_core = -1; + bt_osal_event_init(&ev, ev_cb_core, NULL); + bt_osal_eventq_put(&evq, &ev); + + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_INT(0, s_last_core); + + bt_osal_event_deinit(&ev); + bt_osal_eventq_deinit(&evq); + + osal_test_end(); +} + +TEST_CASE("osal eventq_post_func runs a one-shot handler", "[osal]") +{ + struct bt_osal_eventq evq = {0}; + + osal_test_begin(); + + bt_osal_eventq_init(&evq); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, + bt_osal_eventq_start(&evq, &s_test_task_info)); + + TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, bt_osal_eventq_post_func(&evq, NULL, NULL)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, bt_osal_eventq_post_func(NULL, ev_cb_count, NULL)); + + /* The event is allocated by the layer and freed after the handler returns; + * a leak would be caught by the heap check in tearDown(). + */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, + bt_osal_eventq_post_func(&evq, ev_cb_count, NULL)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, + bt_osal_eventq_post_func(&evq, ev_cb_count, (void *)0xABCD)); + + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(2, s_run_count); + TEST_ASSERT_EQUAL_PTR((void *)0xABCD, s_last_arg); + + bt_osal_eventq_deinit(&evq); + + osal_test_end(); +} + +TEST_CASE("osal eventq_deinit reclaims still-queued post_func items", "[osal]") +{ + struct bt_osal_eventq evq = {0}; + + osal_test_begin(); + + bt_osal_eventq_init(&evq); + + /* Post one-shot items but never start a worker, so they stay queued and are + * never dispatched. Their backing storage is owned by the layer, so + * eventq_deinit() must reclaim it; otherwise the heap check in tearDown() + * would report a leak. + */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_eventq_post_func(&evq, ev_cb_count, NULL)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_eventq_post_func(&evq, ev_cb_count, (void *)0x1)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_eventq_post_func(&evq, ev_cb_count, (void *)0x2)); + TEST_ASSERT_FALSE(bt_osal_eventq_is_empty(&evq)); + + bt_osal_eventq_deinit(&evq); + TEST_ASSERT_NULL(evq.eventq); + /* The items were reclaimed on teardown, not dispatched. */ + TEST_ASSERT_EQUAL_UINT32(0, s_run_count); + + osal_test_end(); +} + +/* Producer for the concurrency test: posts every event of the array to the queue. */ +static void eventq_producer_task(void *arg) +{ + struct bt_osal_eventq *evq = (struct bt_osal_eventq *)arg; + static struct bt_osal_event evs[8]; + + for (int i = 0; i < 8; i++) { + evs[i] = (struct bt_osal_event){0}; + bt_osal_event_init(&evs[i], ev_cb_count, (void *)(uintptr_t)i); + bt_osal_eventq_put(evq, &evs[i]); + } + + /* Hand the events back to the test task so it can release them. */ + s_last_arg = evs; + xSemaphoreGive(s_done); + vTaskDelete(NULL); +} + +TEST_CASE("osal eventq delivers every event under a concurrent producer", "[osal]") +{ + struct bt_osal_eventq evq = {0}; + struct bt_osal_event *evs; + bool seen[8] = {false}; + + osal_test_begin(); + + bt_osal_eventq_init(&evq); + + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(eventq_producer_task, "osal_prod", TEST_HELPER_STACK, + &evq, uxTaskPriorityGet(NULL), NULL)); + + /* Drain the queue: each event must come out exactly once. */ + for (int i = 0; i < 8; i++) { + struct bt_osal_event *ev = bt_osal_eventq_get(&evq, pdMS_TO_TICKS(TEST_WAIT_MS)); + TEST_ASSERT_NOT_NULL(ev); + + uintptr_t idx = (uintptr_t)bt_osal_event_get_arg(ev); + TEST_ASSERT_LESS_THAN_UINT32(8, idx); + TEST_ASSERT_FALSE(seen[idx]); + seen[idx] = true; + } + TEST_ASSERT_TRUE(bt_osal_eventq_is_empty(&evq)); + + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + evs = (struct bt_osal_event *)s_last_arg; + for (int i = 0; i < 8; i++) { + bt_osal_event_deinit(&evs[i]); + } + + bt_osal_eventq_deinit(&evq); + + osal_test_end(); +} + +/* Holds the mutex until the test task tells it to let go. */ +static SemaphoreHandle_t s_holder_ready; + +static void mutex_holder_task(void *arg) +{ + struct bt_osal_mutex *mu = (struct bt_osal_mutex *)arg; + + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_pend(mu, BT_OSAL_TIME_FOREVER)); + xSemaphoreGive(s_holder_ready); + + /* Wait for the test task to finish its contended pend, then hand the mutex back. */ + vTaskDelay(pdMS_TO_TICKS(100)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_release(mu)); + + xSemaphoreGive(s_done); + vTaskDelete(NULL); +} + +TEST_CASE("osal mutex is recursive and pend times out while held", "[osal]") +{ + struct bt_osal_mutex mu = {0}; + struct bt_osal_mutex not_inited = {0}; + + osal_test_begin(); + + /* Operations on a mutex that was never initialized are rejected. */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_mutex_pend(¬_inited, 0)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_mutex_release(¬_inited)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_mutex_deinit(¬_inited)); + + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_init(&mu)); + TEST_ASSERT_NOT_NULL(mu.mutex); + + /* The owner may take it again without deadlocking. */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_pend(&mu, 0)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_pend(&mu, 0)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_release(&mu)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_release(&mu)); + + s_holder_ready = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_holder_ready); + + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(mutex_holder_task, "osal_hold", TEST_HELPER_STACK, + &mu, uxTaskPriorityGet(NULL), NULL)); + + /* Another task owns the mutex now: a bounded pend must give up. */ + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_holder_ready, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_INT(BT_OSAL_TIMEOUT, bt_osal_mutex_pend(&mu, pdMS_TO_TICKS(TEST_NO_EVENT_MS))); + + /* Once the holder releases it, the same pend succeeds. */ + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_pend(&mu, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_release(&mu)); + + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_deinit(&mu)); + TEST_ASSERT_NULL(mu.mutex); + + vSemaphoreDelete(s_holder_ready); + s_holder_ready = NULL; + + osal_test_end(); +} + +static void sem_releaser_task(void *arg) +{ + struct bt_osal_sem *sem = (struct bt_osal_sem *)arg; + + vTaskDelay(pdMS_TO_TICKS(50)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_release(sem)); + + vTaskDelete(NULL); +} + +TEST_CASE("osal semaphore counts tokens and blocks until released", "[osal]") +{ + struct bt_osal_sem sem = {0}; + struct bt_osal_sem not_inited = {0}; + + osal_test_begin(); + + TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_sem_pend(¬_inited, 0)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_sem_release(¬_inited)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_sem_deinit(¬_inited)); + + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_init(&sem, 2)); + TEST_ASSERT_NOT_NULL(sem.sem); + TEST_ASSERT_EQUAL_UINT16(2, bt_osal_sem_get_count(&sem)); + + /* Drain the initial tokens. */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_pend(&sem, 0)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_pend(&sem, 0)); + TEST_ASSERT_EQUAL_UINT16(0, bt_osal_sem_get_count(&sem)); + + /* No token left: a bounded pend gives up. */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_TIMEOUT, bt_osal_sem_pend(&sem, pdMS_TO_TICKS(TEST_NO_EVENT_MS))); + + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_release(&sem)); + TEST_ASSERT_EQUAL_UINT16(1, bt_osal_sem_get_count(&sem)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_pend(&sem, 0)); + + /* A release from another task wakes a blocked pend. */ + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(sem_releaser_task, "osal_sem", TEST_HELPER_STACK, + &sem, uxTaskPriorityGet(NULL), NULL)); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_pend(&sem, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT16(0, bt_osal_sem_get_count(&sem)); + + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_deinit(&sem)); + TEST_ASSERT_NULL(sem.sem); + + osal_test_end(); +} + +TEST_CASE("osal callout posts its event to the event queue on expiry", "[osal]") +{ + struct bt_osal_eventq evq = {0}; + struct bt_osal_callout co = {0}; + struct bt_osal_event *ev; + bt_osal_time_t ticks; + bt_osal_time_t remaining; + + osal_test_begin(); + + bt_osal_eventq_init(&evq); + TEST_ASSERT_EQUAL_INT(0, bt_osal_callout_init(&co, &evq, ev_cb_count, (void *)0x99)); + TEST_ASSERT_NOT_NULL(co.co); + TEST_ASSERT_FALSE(bt_osal_callout_is_active(&co)); + + ticks = bt_osal_time_ms_to_ticks32(200); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_callout_reset(&co, ticks)); + + /* The FreeRTOS timer daemon arms the timer asynchronously, so let it run + * before inspecting the callout's state. + */ + vTaskDelay(pdMS_TO_TICKS(20)); + TEST_ASSERT_TRUE(bt_osal_callout_is_active(&co)); + + /* An armed callout expires in the future and has time left to run. */ + TEST_ASSERT_GREATER_THAN_UINT32(bt_osal_time_get(), bt_osal_callout_get_ticks(&co)); + remaining = bt_osal_callout_remaining_ticks(&co, bt_osal_time_get()); + TEST_ASSERT_GREATER_THAN_UINT32(0, remaining); + TEST_ASSERT_LESS_OR_EQUAL_UINT32(ticks, remaining); + + /* Nothing is delivered before the callout expires. */ + TEST_ASSERT_NULL(bt_osal_eventq_get(&evq, pdMS_TO_TICKS(TEST_NO_EVENT_MS))); + + /* On expiry the callout's event lands in the queue, ready to be run. */ + ev = bt_osal_eventq_get(&evq, pdMS_TO_TICKS(TEST_WAIT_MS)); + TEST_ASSERT_NOT_NULL(ev); + bt_osal_event_run(ev); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + TEST_ASSERT_EQUAL_PTR((void *)0x99, s_last_arg); + /* A callout is one-shot. */ + TEST_ASSERT_FALSE(bt_osal_callout_is_active(&co)); + + /* Stopping an armed callout keeps its event out of the queue. */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_callout_reset(&co, bt_osal_time_ms_to_ticks32(100))); + bt_osal_callout_stop(&co); + vTaskDelay(pdMS_TO_TICKS(20)); + TEST_ASSERT_FALSE(bt_osal_callout_is_active(&co)); + TEST_ASSERT_NULL(bt_osal_eventq_get(&evq, pdMS_TO_TICKS(200))); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + + bt_osal_callout_deinit(&co); + TEST_ASSERT_NULL(co.co); + /* Deinit of an already released callout is a no-op. */ + bt_osal_callout_deinit(&co); + + bt_osal_eventq_deinit(&evq); + + osal_test_end(); +} + +TEST_CASE("osal callout without an event queue runs its handler inline", "[osal]") +{ + struct bt_osal_callout co = {0}; + + osal_test_begin(); + + /* With no event queue the handler is called straight from timer context. */ + TEST_ASSERT_EQUAL_INT(0, bt_osal_callout_init(&co, NULL, ev_cb_count, (void *)0x77)); + + bt_osal_callout_set_arg(&co, (void *)0x88); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_callout_reset(&co, bt_osal_time_ms_to_ticks32(50))); + + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + TEST_ASSERT_EQUAL_PTR((void *)0x88, s_last_arg); + + /* The callout can be re-armed after firing. */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_callout_reset(&co, bt_osal_time_ms_to_ticks32(50))); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(2, s_run_count); + + bt_osal_callout_deinit(&co); + + osal_test_end(); +} + +TEST_CASE("osal time helpers round-trip between ms and ticks", "[osal]") +{ + bt_osal_time_t ticks = 0; + uint32_t ms = 0; + uint32_t elapsed; + uint32_t start; + + osal_test_begin(); + + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_time_ms_to_ticks(1000, &ticks)); + TEST_ASSERT_GREATER_THAN_UINT32(0, ticks); + TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_time_ticks_to_ms(ticks, &ms)); + TEST_ASSERT_EQUAL_UINT32(1000, ms); + + /* The 32-bit variants must agree with the checked ones. */ + TEST_ASSERT_EQUAL_UINT32(ticks, bt_osal_time_ms_to_ticks32(1000)); + TEST_ASSERT_EQUAL_UINT32(1000, bt_osal_time_ticks_to_ms32(ticks)); + +#if !BT_OSAL_USE_ESP_TIMER && (configTICK_RATE_HZ < 1000) + /* One tick is more than one millisecond here, so a full-range tick count + * cannot be expressed in milliseconds. + */ + TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, bt_osal_time_ticks_to_ms(UINT32_MAX, &ms)); +#endif + + /* The clock advances by at least the requested delay. */ + start = bt_osal_time_get(); + bt_osal_time_delay(bt_osal_time_ms_to_ticks32(50)); + elapsed = bt_osal_time_get() - start; + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(bt_osal_time_ms_to_ticks32(40), elapsed); + + osal_test_end(); +} + +TEST_CASE("osal reports OS state and nests critical sections", "[osal]") +{ + uint32_t outer; + uint32_t inner; + bool in_outer; + bool in_inner; + bool after_inner; + bool after_outer; + + osal_test_begin(); + + TEST_ASSERT_TRUE(bt_osal_os_started()); + TEST_ASSERT_EQUAL_PTR(xTaskGetCurrentTaskHandle(), bt_osal_get_current_task_id()); + TEST_ASSERT_EQUAL_UINT32(portMAX_DELAY, BT_OSAL_TIME_FOREVER); + TEST_ASSERT_FALSE(bt_osal_hw_is_in_critical()); + + /* Sample the state inside the critical section but assert outside it: a failing + * assertion longjmps out of the test and would leave the spinlock held. + */ + outer = bt_osal_hw_enter_critical(); + in_outer = bt_osal_hw_is_in_critical(); + inner = bt_osal_hw_enter_critical(); + in_inner = bt_osal_hw_is_in_critical(); + bt_osal_hw_exit_critical(inner); + after_inner = bt_osal_hw_is_in_critical(); + bt_osal_hw_exit_critical(outer); + after_outer = bt_osal_hw_is_in_critical(); + + TEST_ASSERT_TRUE(in_outer); + TEST_ASSERT_TRUE(in_inner); + /* Leaving the inner section must not clear the outer one. */ + TEST_ASSERT_TRUE(after_inner); + TEST_ASSERT_FALSE(after_outer); + + osal_test_end(); +} diff --git a/components/bt/test_apps/basic_unit_test/main/test_prf_task.c b/components/bt/test_apps/basic_unit_test/main/test_prf_task.c new file mode 100644 index 00000000000..6fc03c5ed1c --- /dev/null +++ b/components/bt/test_apps/basic_unit_test/main/test_prf_task.c @@ -0,0 +1,172 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* + * Unit tests for the shared BLE profile task (components/bt/ble_profiles/common): + * lifecycle, the running-state guard on the shared queue, and the caller-owned + * (zero-allocation) event path. + * + * Each test brings the OSAL function table up (bt_prf_task_init() dispatches + * through it) and tears it down again, so the per-test heap check in tearDown() + * also covers the profile task's own allocations. + */ + +#include +#include + +#include "unity.h" +#include "unity_test_runner.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" + +#include "esp_err.h" +#include "bt_osal.h" +#include "bt_osal_freertos.h" +#include "bt_prf_task.h" + +/* Long enough for the worker to run the handler, short enough to stay quick. */ +#define TEST_WAIT_MS 1000 +/* Waited on when a handler is expected NOT to run. */ +#define TEST_NO_RUN_MS 50 + +/* Shared state between the test task and the posted handlers. */ +static SemaphoreHandle_t s_done; +static volatile uint32_t s_run_count; +static void *s_last_arg; +static TaskHandle_t s_run_task; + +static void prf_test_begin(void) +{ + s_run_count = 0; + s_last_arg = NULL; + s_run_task = NULL; + + s_done = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_done); + + bt_osal_freertos_funcs_init(); + TEST_ASSERT_NOT_NULL(bt_osal_freertos_funcs_get()); +} + +static void prf_test_end(void) +{ + /* Safe even if a test already deinitialized it. */ + TEST_ASSERT_EQUAL(ESP_OK, bt_prf_task_deinit()); + + bt_osal_freertos_funcs_deinit(); + TEST_ASSERT_NULL(bt_osal_freertos_funcs_get()); + + vSemaphoreDelete(s_done); + s_done = NULL; + + /* The worker task is deleted by bt_prf_task_deinit(); its TCB/stack are + * reclaimed asynchronously by the idle task, so give it a moment before the + * heap check in tearDown() runs. */ + vTaskDelay(pdMS_TO_TICKS(20)); +} + +/* Fire-and-forget handler: records the argument and the task it ran on. */ +static void handler_record(struct bt_osal_event *ev) +{ + s_run_count++; + s_last_arg = bt_osal_event_get_arg(ev); + s_run_task = xTaskGetCurrentTaskHandle(); + xSemaphoreGive(s_done); +} + +TEST_CASE("prf_task init/deinit lifecycle", "[prf_task]") +{ + prf_test_begin(); + + TEST_ASSERT_FALSE(bt_prf_task_is_running()); + TEST_ASSERT_NULL(bt_prf_task_eventq()); + + TEST_ASSERT_EQUAL(ESP_OK, bt_prf_task_init()); + TEST_ASSERT_TRUE(bt_prf_task_is_running()); + TEST_ASSERT_NOT_NULL(bt_prf_task_eventq()); + + /* Second init while already running is an idempotent no-op. */ + TEST_ASSERT_EQUAL(ESP_OK, bt_prf_task_init()); + TEST_ASSERT_TRUE(bt_prf_task_is_running()); + + TEST_ASSERT_EQUAL(ESP_OK, bt_prf_task_deinit()); + TEST_ASSERT_FALSE(bt_prf_task_is_running()); + TEST_ASSERT_NULL(bt_prf_task_eventq()); + + /* Deinit when not running is a no-op. */ + TEST_ASSERT_EQUAL(ESP_OK, bt_prf_task_deinit()); + + prf_test_end(); +} + +TEST_CASE("prf_task eventq post runs handler on the worker task", "[prf_task]") +{ + struct bt_osal_event ev = {0}; + struct bt_osal_eventq *evq; + + prf_test_begin(); + + TEST_ASSERT_EQUAL(ESP_OK, bt_prf_task_init()); + evq = bt_prf_task_eventq(); + TEST_ASSERT_NOT_NULL(evq); + + /* Post a caller-owned event straight onto the shared queue. */ + bt_osal_event_init(&ev, handler_record, (void *)0xABCD); + bt_osal_eventq_put(evq, &ev); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + TEST_ASSERT_EQUAL_PTR((void *)0xABCD, s_last_arg); + /* The handler must run on the worker task, not the caller's context. */ + TEST_ASSERT_NOT_NULL(s_run_task); + TEST_ASSERT_NOT_EQUAL(xTaskGetCurrentTaskHandle(), s_run_task); + + bt_osal_event_deinit(&ev); + + prf_test_end(); +} + +TEST_CASE("prf_task eventq is available only while running", "[prf_task]") +{ + prf_test_begin(); + + /* No queue to post to before the task is running. */ + TEST_ASSERT_NULL(bt_prf_task_eventq()); + + TEST_ASSERT_EQUAL(ESP_OK, bt_prf_task_init()); + TEST_ASSERT_NOT_NULL(bt_prf_task_eventq()); + + /* The queue is gone again after teardown. */ + TEST_ASSERT_EQUAL(ESP_OK, bt_prf_task_deinit()); + TEST_ASSERT_NULL(bt_prf_task_eventq()); + + prf_test_end(); +} + +TEST_CASE("prf_task drives a caller-owned event (zero allocation)", "[prf_task]") +{ + struct bt_osal_event ev = {0}; + struct bt_osal_eventq *evq; + + prf_test_begin(); + + TEST_ASSERT_EQUAL(ESP_OK, bt_prf_task_init()); + evq = bt_prf_task_eventq(); + TEST_ASSERT_NOT_NULL(evq); + + /* Post a caller-owned event straight onto the shared queue. */ + bt_osal_event_init(&ev, handler_record, (void *)0x1234); + bt_osal_eventq_put(evq, &ev); + + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + TEST_ASSERT_EQUAL_PTR((void *)0x1234, s_last_arg); + + bt_osal_event_deinit(&ev); + + prf_test_end(); +} diff --git a/components/bt/test_apps/basic_unit_test/sdkconfig.defaults b/components/bt/test_apps/basic_unit_test/sdkconfig.defaults index dc2d7ca08b5..f379cce0614 100644 --- a/components/bt/test_apps/basic_unit_test/sdkconfig.defaults +++ b/components/bt/test_apps/basic_unit_test/sdkconfig.defaults @@ -1,2 +1,3 @@ CONFIG_BT_ENABLED=y +CONFIG_BT_PRF_TASK_ENABLED=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n