diff --git a/components/bt/CMakeLists.txt b/components/bt/CMakeLists.txt index 25919fd36dd..4b292e45ceb 100644 --- a/components/bt/CMakeLists.txt +++ b/components/bt/CMakeLists.txt @@ -71,6 +71,11 @@ if(CONFIG_IDF_DOC_BUILD OR CONFIG_BT_ENABLED) list(APPEND srcs ${ble_audio_srcs}) list(APPEND include_dirs ${ble_audio_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 5e74f5ea6c4..1a811993b50 100644 --- a/components/bt/Kconfig +++ b/components/bt/Kconfig @@ -119,6 +119,11 @@ menu "Bluetooth" source "$IDF_PATH/components/bt/esp_ble_audio/Kconfig.in" endmenu + menu "BLE Profiles" + depends on BT_ENABLED + source "$IDF_PATH/components/bt/ble_profiles/Kconfig.in" + endmenu + config BT_HCI_LOG_INSIGHTS_ENABLE depends on BT_HCI_LOG_DEBUG_EN bool "Enable Insights for HCI LOGS BT Stack" 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 9c7e04d1ed7..ecd8ebb679d 100644 --- a/components/bt/common/Kconfig.in +++ b/components/bt/common/Kconfig.in @@ -6,6 +6,36 @@ config BT_ALARM_MAX_NUM This option decides the maximum number of alarms which could be used by Bluetooth host. +config BT_BLE_HOST_ALLOW_SUB_SPEC_MIN_CONN_INT + bool "Allow BLE connection interval below Bluetooth Core Spec minimum (disable host check)" + depends on BT_BLE_ENABLED || BT_NIMBLE_ENABLED + default n + help + When enabled, BLE host-side validation accepts connection interval + values below the Bluetooth Core Specification minimum of 0x0006 + (7.5 ms), down to non-zero values. The BLE controller still enforces + what is actually supported in hardware and firmware. + + End users should NOT set this option directly. In typical IDF builds it + follows the active Controller integration when that Controller supports + 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/Kconfig.in b/components/bt/host/bluedroid/Kconfig.in index 90ab7269fbe..91ab3248f02 100644 --- a/components/bt/host/bluedroid/Kconfig.in +++ b/components/bt/host/bluedroid/Kconfig.in @@ -1434,23 +1434,6 @@ config BT_BLE_RPA_TIMEOUT This set RPA timeout of Controller and Host. Default is 900 s (15 minutes). Range is 1 s to 1 hour (3600 s). -config BT_BLE_HOST_ALLOW_SUB_SPEC_MIN_CONN_INT - bool "Allow BLE connection interval below Bluetooth Core Spec minimum (disable host check)" - depends on BT_BLE_ENABLED - default n - help - When enabled, the Bluedroid host skips the minimum BLE connection - interval validation (Bluetooth Core Spec minimum is 0x0006 / 7.5 ms) - and accepts any non-zero interval value from the application. The - BLE controller then enforces what is actually allowed; how small the - connection interval may be depends on controller capability and its - own configuration, not on this host option text. - - End users should NOT set this option directly. In typical IDF builds it - follows the active Controller integration when that Controller supports - this mode; use the Controller's own configuration (menu entries and symbol - names differ by chip) instead of toggling this host symbol manually. - menuconfig BT_BLE_50_FEATURES_SUPPORTED bool "Enable BLE 5.0 and above features(please disable BLE 4.2 if enable BLE 5.0)" depends on (BT_BLE_ENABLED && ((BT_CONTROLLER_ENABLED && SOC_BLE_50_SUPPORTED) || BT_CONTROLLER_DISABLED)) diff --git a/components/bt/host/bluedroid/api/esp_bt_main.c b/components/bt/host/bluedroid/api/esp_bt_main.c index 6db37fd2a1a..fa4a25b4ef1 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; @@ -233,6 +235,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; @@ -289,6 +302,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/Kconfig.in b/components/bt/host/nimble/Kconfig.in index 3f558a31d28..8c89961d857 100644 --- a/components/bt/host/nimble/Kconfig.in +++ b/components/bt/host/nimble/Kconfig.in @@ -349,6 +349,16 @@ menu "GAP" Enable this option to send number-of-completed-packets event to controller after disconnection + config BT_NIMBLE_DEFER_CONN_EVENTS_UNTIL_CONNECT + bool "Defer connection GAP/ATT events until CONNECT callback" + depends on BT_NIMBLE_ENABLED + default y + help + Queue connection-related GAP callbacks and ATT server requests until + BLE_GAP_EVENT_CONNECT event is delivered for that connection handle. + Required when the host delays CONNECT so applications never receive other + events before CONNECT event. + endmenu #GAP menu "GATT / ATT" @@ -416,28 +426,28 @@ menu "GATT / ATT" config BT_NIMBLE_GATT_CACHING_MAX_SVCS int "Maximum number of services per connection" depends on BT_NIMBLE_GATT_CACHING - default 64 + default 8 help Set this option to set the upper limit on number of services per connection to be cached. config BT_NIMBLE_GATT_CACHING_MAX_INCL_SVCS int "Maximum number of included services per connection" depends on BT_NIMBLE_GATT_CACHING - default 64 + default 8 help Set this option to set the upper limit on number of included services per connection to be cached. config BT_NIMBLE_GATT_CACHING_MAX_CHRS int "Maximum number of characteristics per connection" depends on BT_NIMBLE_GATT_CACHING - default 64 + default 58 help Set this option to set the upper limit on number of characteristics per connection to be cached. config BT_NIMBLE_GATT_CACHING_MAX_DSCS int "Maximum number of descriptors per connection" depends on BT_NIMBLE_GATT_CACHING - default 64 + default 58 help Set this option to set the upper limit on number of descriptors per connection to be cached. @@ -991,6 +1001,20 @@ menu "Services" help Enable the DIS PnP ID characteristic + config BT_NIMBLE_SVC_DIS_IEEE + depends on BT_NIMBLE_DIS_SERVICE + bool "IEEE" + default y + help + Enable the DIS IEEE characteristic + + config BT_NIMBLE_SVC_DIS_UDI + depends on BT_NIMBLE_DIS_SERVICE + bool "UDI" + default y + help + Enable the DIS UDI characteristic + config BT_NIMBLE_SVC_DIS_INCLUDED depends on BT_NIMBLE_DIS_SERVICE bool "DIS as an Included Service" @@ -1332,6 +1356,14 @@ menu "Extra Features" instead of DRAM (.iram.text/.text in internal RAM). Suitable for low-speed GAP/GATT operations to reduce RAM usage. + config BT_NIMBLE_GAP_ALLOW_ADV_RESTART + bool "Allow restarting ongoing advertising" + default n + help + If enabled, stop ongoing advertising before starting it again with + new parameters,if any. If disabled, starting advertising while it is already + active returns BLE_HS_EALREADY. + endmenu menu "NimBLE Mesh" diff --git a/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c b/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c index e81e9d481d9..125486965d9 100644 --- a/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c +++ b/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c @@ -182,7 +182,6 @@ static void ble_hci_rx_acl(uint8_t *data, uint16_t len) { struct os_mbuf *m = NULL; int rc; - int sr; int retry_count = 1; @@ -216,9 +215,7 @@ static void ble_hci_rx_acl(uint8_t *data, uint16_t len) os_mbuf_free_chain(m); return; } - OS_ENTER_CRITICAL(sr); ble_transport_to_hs_acl(m); - OS_EXIT_CRITICAL(sr); } #endif diff --git a/components/bt/host/nimble/nimble b/components/bt/host/nimble/nimble index 139cada0ae9..96ab0e4d864 160000 --- a/components/bt/host/nimble/nimble +++ b/components/bt/host/nimble/nimble @@ -1 +1 @@ -Subproject commit 139cada0ae932957fa06ba37d17e3c9c2c95c773 +Subproject commit 96ab0e4d864090777512cddbb4aa8609d598c000 diff --git a/components/bt/host/nimble/port/include/esp_nimble_cfg.h b/components/bt/host/nimble/port/include/esp_nimble_cfg.h index a0f62f36aa2..e6b1e2d12c9 100644 --- a/components/bt/host/nimble/port/include/esp_nimble_cfg.h +++ b/components/bt/host/nimble/port/include/esp_nimble_cfg.h @@ -1842,6 +1842,18 @@ #define MYNEWT_VAL_BLE_SVC_DIS_PNP_ID_READ_PERM (-1) #endif +#if CONFIG_BT_NIMBLE_SVC_DIS_IEEE +#define MYNEWT_VAL_BLE_SVC_DIS_IEEE_READ_PERM (0) +#else +#define MYNEWT_VAL_BLE_SVC_DIS_IEEE_READ_PERM (-1) +#endif + +#if CONFIG_BT_NIMBLE_SVC_DIS_UDI +#define MYNEWT_VAL_BLE_SVC_DIS_UDI_READ_PERM (0) +#else +#define MYNEWT_VAL_BLE_SVC_DIS_UDI_READ_PERM (-1) +#endif + #ifndef MYNEWT_VAL_BLE_SVC_DIS_INCLUDED #define MYNEWT_VAL_BLE_SVC_DIS_INCLUDED (CONFIG_BT_NIMBLE_SVC_DIS_INCLUDED) #endif @@ -2390,4 +2402,20 @@ #endif #endif +#ifndef MYNEWT_VAL_BLE_DEFER_CONN_EVENTS +#ifdef CONFIG_BT_NIMBLE_DEFER_CONN_EVENTS_UNTIL_CONNECT +#define MYNEWT_VAL_BLE_DEFER_CONN_EVENTS CONFIG_BT_NIMBLE_DEFER_CONN_EVENTS_UNTIL_CONNECT +#else +#define MYNEWT_VAL_BLE_DEFER_CONN_EVENTS (0) +#endif +#endif + +#ifndef MYNEWT_VAL_BLE_GAP_ALLOW_ADV_RESTART +#ifdef CONFIG_BT_NIMBLE_GAP_ALLOW_ADV_RESTART +#define MYNEWT_VAL_BLE_GAP_ALLOW_ADV_RESTART CONFIG_BT_NIMBLE_GAP_ALLOW_ADV_RESTART +#else +#define MYNEWT_VAL_BLE_GAP_ALLOW_ADV_RESTART (0) +#endif +#endif + #endif diff --git a/components/bt/host/nimble/port/include/esp_nimble_mem.h b/components/bt/host/nimble/port/include/esp_nimble_mem.h index dd60c741cfe..bc57004da8e 100644 --- a/components/bt/host/nimble/port/include/esp_nimble_mem.h +++ b/components/bt/host/nimble/port/include/esp_nimble_mem.h @@ -17,6 +17,8 @@ void *nimble_mem_malloc(size_t size); void *nimble_mem_calloc(size_t n, size_t size); +void *nimble_mem_realloc(void *ptr, size_t size); + void nimble_mem_free(void *ptr); #if CONFIG_BT_LE_USED_MEM_STATISTICS_ENABLED @@ -89,17 +91,6 @@ void nimble_mem_dbg_set_section_end(uint8_t index); */ uint32_t nimble_mem_dbg_get_max_size_section(uint8_t index); -/** - * @brief Reallocate memory with debug tracking - * - * @param ptr Pointer to memory to reallocate - * @param new_size New size of allocation - * @param func Function name where realloc occurred - * @param line Line number where realloc occurred - * @return Pointer to reallocated memory - */ -void *nimble_mem_dbg_realloc(void *ptr, size_t new_size, const char *func, int line); - #endif // CONFIG_BT_NIMBLE_MEM_DEBUG @@ -127,11 +118,17 @@ void *nimble_mem_dbg_realloc(void *ptr, size_t new_size, const char *func, int l #define nimble_platform_mem_realloc(ptr, new_size) \ ({ \ - void *p; \ - do { \ - p = nimble_mem_dbg_realloc(ptr, new_size, __func__, __LINE__); \ - } while (0); \ - p; \ + void *_old = (void *)(ptr); \ + size_t _nsz = (size_t)(new_size); \ + void *_new = nimble_mem_realloc(_old, _nsz); \ + if (_new == NULL && _nsz > 0) { \ + /* realloc failed: original block still alive, keep its debug record */ \ + } else { \ + /* success or free (new_size==0): clean old, record new */ \ + if (_old) nimble_mem_dbg_clean(_old, __func__, __LINE__); \ + if (_new) nimble_mem_dbg_record(_new, _nsz, __func__, __LINE__); \ + } \ + _new; \ }) #define nimble_platform_mem_free(ptr) \ @@ -145,7 +142,7 @@ do { \ #define nimble_platform_mem_malloc nimble_mem_malloc #define nimble_platform_mem_calloc nimble_mem_calloc -#define nimble_platform_mem_realloc realloc +#define nimble_platform_mem_realloc nimble_mem_realloc #define nimble_platform_mem_free nimble_mem_free #endif // CONFIG_BT_NIMBLE_MEM_DEBUG diff --git a/components/bt/host/nimble/port/src/esp_nimble_mem.c b/components/bt/host/nimble/port/src/esp_nimble_mem.c index 3e1ed6f46aa..05d96c9a80e 100644 --- a/components/bt/host/nimble/port/src/esp_nimble_mem.c +++ b/components/bt/host/nimble/port/src/esp_nimble_mem.c @@ -17,7 +17,7 @@ static size_t host_mem_used_size = 0; #if CONFIG_BT_NIMBLE_MEM_DEBUG -#define NIMBLE_MEM_DBG_INFO_MAX 1024*3 +#define NIMBLE_MEM_DBG_INFO_MAX (1024*3) typedef struct { void *p; int size; @@ -79,6 +79,7 @@ void nimble_mem_dbg_record(void *p, int size, const char *func, int line) if (i >= NIMBLE_MEM_DBG_INFO_MAX) { ESP_LOGE("BT_NIMBLE_MEM", "%s full %s %d !!\n", __func__, func, line); + return; } nimble_mem_dbg_current_size += size; @@ -190,66 +191,6 @@ uint32_t nimble_mem_dbg_get_max_size_section(uint8_t index) return nimble_mem_dbg_max_size_section[index].max_size; } -void *nimble_mem_dbg_realloc(void *ptr, size_t new_size, const char *func, int line) -{ - size_t old_size = 0; - int i; - - void *new_ptr = realloc(ptr, new_size); - if (new_ptr == NULL && new_size > 0) { - // realloc failed, keep old ptr record - return NULL; - } - - // Find and clean old record if ptr is not NULL - if (ptr != NULL) { - for (i = 0; i < NIMBLE_MEM_DBG_INFO_MAX; i++) { - if (nimble_mem_dbg_info[i].p == ptr) { - old_size = nimble_mem_dbg_info[i].size; - nimble_mem_dbg_current_size -= old_size; - - nimble_mem_dbg_info[i].p = NULL; - nimble_mem_dbg_info[i].size = 0; - nimble_mem_dbg_info[i].func = NULL; - nimble_mem_dbg_info[i].line = 0; - nimble_mem_dbg_count--; - break; - } - } - } - - // Record the new allocation if new_size > 0 - if (new_ptr != NULL && new_size > 0) { - for (i = 0; i < NIMBLE_MEM_DBG_INFO_MAX; i++) { - if (nimble_mem_dbg_info[i].p == NULL) { - nimble_mem_dbg_info[i].p = new_ptr; - nimble_mem_dbg_info[i].size = new_size; - nimble_mem_dbg_info[i].func = func; - nimble_mem_dbg_info[i].line = line; - nimble_mem_dbg_count++; - break; - } - } - - if (i >= NIMBLE_MEM_DBG_INFO_MAX) { - ESP_LOGE("BT_NIMBLE_MEM", "%s full %s %d !!\n", __func__, func, line); - } - - nimble_mem_dbg_current_size += new_size; - if (nimble_mem_dbg_max_size < nimble_mem_dbg_current_size) { - nimble_mem_dbg_max_size = nimble_mem_dbg_current_size; - } - - for (i = 0; i < NIMBLE_MEM_DBG_MAX_SECTION_NUM; i++) { - if (nimble_mem_dbg_max_size_section[i].used && - nimble_mem_dbg_max_size_section[i].max_size < nimble_mem_dbg_current_size) { - nimble_mem_dbg_max_size_section[i].max_size = nimble_mem_dbg_current_size; - } - } - } - - return new_ptr; -} #endif // CONFIG_BT_NIMBLE_MEM_DEBUG #if !CONFIG_BT_NIMBLE_LOW_SPEED_MODE @@ -306,6 +247,44 @@ void *nimble_mem_calloc(size_t n, size_t size) return mem; } +#if !CONFIG_BT_NIMBLE_LOW_SPEED_MODE +IRAM_ATTR +#endif +void *nimble_mem_realloc(void *ptr, size_t size) +{ + void *mem = NULL; +#if CONFIG_BT_LE_USED_MEM_STATISTICS_ENABLED + size_t old_size = 0; + if (ptr) { + old_size = heap_caps_get_allocated_size(ptr); + } +#endif + +#ifdef CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_INTERNAL + mem = heap_caps_realloc(ptr, size, MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT); +#elif CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL + mem = heap_caps_realloc(ptr, size, MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT); +#elif CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_IRAM_8BIT + mem = heap_caps_realloc_prefer(ptr, size, 2, + MALLOC_CAP_INTERNAL|MALLOC_CAP_IRAM_8BIT, + MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT); +#else + mem = realloc(ptr, size); +#endif + +#if CONFIG_BT_LE_USED_MEM_STATISTICS_ENABLED + if (mem) { + size_t new_size = heap_caps_get_allocated_size(mem); + host_mem_used_size = host_mem_used_size - old_size + new_size; + } else if (ptr && size == 0) { + host_mem_used_size -= old_size; + } +#endif // CONFIG_BT_LE_USED_MEM_STATISTICS_ENABLED + + return mem; +} + + #if !CONFIG_BT_NIMBLE_LOW_SPEED_MODE IRAM_ATTR #endif diff --git a/components/bt/porting/mem/os_msys_init.c b/components/bt/porting/mem/os_msys_init.c index c4574ca5ae4..f492b9572d3 100644 --- a/components/bt/porting/mem/os_msys_init.c +++ b/components/bt/porting/mem/os_msys_init.c @@ -312,7 +312,7 @@ os_msys_buf_free(void) #if OS_MSYS_2_BLOCK_COUNT > 0 bt_osi_mem_free(os_msys_init_2_data); os_msys_init_2_data = NULL; - os_mempool_unregister(&os_msys_init_1_mempool); + os_mempool_unregister(&os_msys_init_2_mempool); #endif #endif // CONFIG_BT_NIMBLE_STATIC_TO_DYNAMIC } diff --git a/components/bt/porting/npl/freertos/src/npl_os_freertos.c b/components/bt/porting/npl/freertos/src/npl_os_freertos.c index 5430647bb3e..5a591ed9412 100644 --- a/components/bt/porting/npl/freertos/src/npl_os_freertos.c +++ b/components/bt/porting/npl/freertos/src/npl_os_freertos.c @@ -25,6 +25,9 @@ portMUX_TYPE ble_port_mutex = portMUX_INITIALIZER_UNLOCKED; +static SemaphoreHandle_t npl_eventq_sync; +static uint8_t hw_critical_state_status[portNUM_PROCESSORS]; + #if BLE_NPL_USE_ESP_TIMER static const char *TAG = "Timer"; #endif @@ -197,6 +200,103 @@ IRAM_ATTR in_isr(void) return xPortInIsrContext() != 0; } +static void +npl_eventq_sync_init(void) +{ + if (npl_eventq_sync == NULL) { + npl_eventq_sync = xSemaphoreCreateRecursiveMutex(); + BLE_LL_ASSERT(npl_eventq_sync); + } +} + +static bool +npl_eventq_lock(void) +{ + BaseType_t core; + + if (in_isr()) { + return false; + } + + core = xPortGetCoreID(); + if (core >= portNUM_PROCESSORS || hw_critical_state_status[core] != 0) { + return false; + } + + BLE_LL_ASSERT(npl_eventq_sync); + xSemaphoreTakeRecursive(npl_eventq_sync, portMAX_DELAY); + return true; +} + +static void +npl_eventq_unlock(bool locked) +{ + if (locked) { + xSemaphoreGiveRecursive(npl_eventq_sync); + } +} + +static bool IRAM_ATTR +npl_eventq_queued_get_isr(struct ble_npl_event_freertos *event) +{ + bool queued; + + portENTER_CRITICAL_ISR(&ble_port_mutex); + queued = event->queued; + portEXIT_CRITICAL_ISR(&ble_port_mutex); + return queued; +} + +static void IRAM_ATTR +npl_eventq_queued_set_task(struct ble_npl_event_freertos *event, bool queued) +{ + portENTER_CRITICAL(&ble_port_mutex); + event->queued = queued; + portEXIT_CRITICAL(&ble_port_mutex); +} + +static bool IRAM_ATTR +npl_eventq_queued_get_task(struct ble_npl_event_freertos *event) +{ + bool queued; + + portENTER_CRITICAL(&ble_port_mutex); + queued = event->queued; + portEXIT_CRITICAL(&ble_port_mutex); + return queued; +} + +static bool IRAM_ATTR +npl_eventq_queued_claim(struct ble_npl_event_freertos *event) +{ + bool already; + + portENTER_CRITICAL(&ble_port_mutex); + already = event->queued; + if (!already) { + event->queued = true; + } + portEXIT_CRITICAL(&ble_port_mutex); + return already; +} + +static void IRAM_ATTR +npl_eventq_lost_event_clear(struct ble_npl_event *ev) +{ + struct ble_npl_event_freertos *lost; + + if (ev == NULL) { + return; + } + + lost = (struct ble_npl_event_freertos *)ev->event; + if (lost == NULL) { + return; + } + + lost->queued = false; +} + struct ble_npl_event * IRAM_ATTR npl_freertos_eventq_get(struct ble_npl_eventq *evq, ble_npl_time_t tmo) { @@ -207,20 +307,74 @@ IRAM_ATTR npl_freertos_eventq_get(struct ble_npl_eventq *evq, ble_npl_time_t tmo if (in_isr()) { BLE_LL_ASSERT(tmo == 0); + woken = pdFALSE; + + portENTER_CRITICAL_ISR(&ble_port_mutex); ret = xQueueReceiveFromISR(eventq->q, &ev, &woken); - if( woken == pdTRUE ) { + if (ret == pdPASS && ev != NULL) { + struct ble_npl_event_freertos *event = (struct ble_npl_event_freertos *)ev->event; + if (event) { + event->queued = false; + } + } + portEXIT_CRITICAL_ISR(&ble_port_mutex); + + if (woken == pdTRUE) { portYIELD_FROM_ISR(); } - } else { - ret = xQueueReceive(eventq->q, &ev, tmo); - } - BLE_LL_ASSERT(ret == pdPASS || ret == errQUEUE_EMPTY); + BLE_LL_ASSERT(ret == pdPASS || ret == errQUEUE_EMPTY); + } else if (tmo == 0) { + bool locked = npl_eventq_lock(); - if (ev) { - struct ble_npl_event_freertos *event = (struct ble_npl_event_freertos *)ev->event; - if (event) { - event->queued = false; - } + portENTER_CRITICAL(&ble_port_mutex); + ret = xQueueReceive(eventq->q, &ev, 0); + if (ret == pdPASS && ev != NULL) { + struct ble_npl_event_freertos *event = (struct ble_npl_event_freertos *)ev->event; + if (event) { + event->queued = false; + } + } + portEXIT_CRITICAL(&ble_port_mutex); + npl_eventq_unlock(locked); + } else { + TickType_t deadline = 0; + TickType_t remaining; + + if (tmo != portMAX_DELAY) { + deadline = xTaskGetTickCount() + tmo; + } + + for (;;) { + if (tmo == portMAX_DELAY) { + ret = xQueuePeek(eventq->q, &ev, portMAX_DELAY); + } else { + remaining = deadline - xTaskGetTickCount(); + if (remaining > tmo) { + return NULL; + } + ret = xQueuePeek(eventq->q, &ev, remaining); + } + if (ret != pdPASS) { + return NULL; + } + + bool locked = npl_eventq_lock(); + + portENTER_CRITICAL(&ble_port_mutex); + ret = xQueueReceive(eventq->q, &ev, 0); + if (ret == pdPASS && ev != NULL) { + struct ble_npl_event_freertos *event = (struct ble_npl_event_freertos *)ev->event; + if (event) { + event->queued = false; + } + } + portEXIT_CRITICAL(&ble_port_mutex); + if (ret == pdPASS && ev != NULL) { + npl_eventq_unlock(locked); + break; + } + npl_eventq_unlock(locked); + } } return ev; @@ -234,22 +388,43 @@ IRAM_ATTR npl_freertos_eventq_put(struct ble_npl_eventq *evq, struct ble_npl_eve struct ble_npl_eventq_freertos *eventq = (struct ble_npl_eventq_freertos *)evq->eventq; struct ble_npl_event_freertos *event = (struct ble_npl_event_freertos *)ev->event; - if (event->queued) { - return; - } - - event->queued = true; - if (in_isr()) { + woken = pdFALSE; + + portENTER_CRITICAL_ISR(&ble_port_mutex); + if (event->queued) { + portEXIT_CRITICAL_ISR(&ble_port_mutex); + return; + } + event->queued = true; + ret = xQueueSendToBackFromISR(eventq->q, &ev, &woken); - if( woken == pdTRUE ) { + if (ret != pdPASS) { + event->queued = false; + portEXIT_CRITICAL_ISR(&ble_port_mutex); + return; + } + portEXIT_CRITICAL_ISR(&ble_port_mutex); + + if (woken == pdTRUE) { portYIELD_FROM_ISR(); } + return; } else { - ret = xQueueSendToBack(eventq->q, &ev, portMAX_DELAY); - } + bool locked = npl_eventq_lock(); - BLE_LL_ASSERT(ret == pdPASS); + if (npl_eventq_queued_claim(event)) { + npl_eventq_unlock(locked); + return; + } + + ret = xQueueSendToBack(eventq->q, &ev, 0); + if (ret != pdPASS) { + ESP_LOGW("NimBLE", "eventq put: queue full, event dropped"); + npl_eventq_queued_set_task(event, false); + } + npl_eventq_unlock(locked); + } } void @@ -260,22 +435,43 @@ IRAM_ATTR npl_freertos_eventq_put_to_front(struct ble_npl_eventq *evq, struct bl struct ble_npl_eventq_freertos *eventq = (struct ble_npl_eventq_freertos *)evq->eventq; struct ble_npl_event_freertos *event = (struct ble_npl_event_freertos *)ev->event; - if (event->queued) { - return; - } - - event->queued = true; - if (in_isr()) { + woken = pdFALSE; + + portENTER_CRITICAL_ISR(&ble_port_mutex); + if (event->queued) { + portEXIT_CRITICAL_ISR(&ble_port_mutex); + return; + } + event->queued = true; + ret = xQueueSendToFrontFromISR(eventq->q, &ev, &woken); - if( woken == pdTRUE ) { + if (ret != pdPASS) { + event->queued = false; + portEXIT_CRITICAL_ISR(&ble_port_mutex); + return; + } + portEXIT_CRITICAL_ISR(&ble_port_mutex); + + if (woken == pdTRUE) { portYIELD_FROM_ISR(); } + return; } else { - ret = xQueueSendToFront(eventq->q, &ev, portMAX_DELAY); - } + bool locked = npl_eventq_lock(); - BLE_LL_ASSERT(ret == pdPASS); + if (npl_eventq_queued_claim(event)) { + npl_eventq_unlock(locked); + return; + } + + ret = xQueueSendToFront(eventq->q, &ev, 0); + if (ret != pdPASS) { + ESP_LOGW("NimBLE", "eventq put_to_front: queue full, event dropped"); + npl_eventq_queued_set_task(event, false); + } + npl_eventq_unlock(locked); + } } void @@ -286,14 +482,11 @@ IRAM_ATTR npl_freertos_eventq_remove(struct ble_npl_eventq *evq, BaseType_t ret; int i; int count; + bool removed; BaseType_t woken, woken2; struct ble_npl_eventq_freertos *eventq = (struct ble_npl_eventq_freertos *)evq->eventq; struct ble_npl_event_freertos *event = (struct ble_npl_event_freertos *)ev->event; - if (!event->queued) { - return; - } - /* * XXX We cannot extract element from inside FreeRTOS queue so as a quick * workaround we'll just remove all elements and add them back except the @@ -302,46 +495,77 @@ IRAM_ATTR npl_freertos_eventq_remove(struct ble_npl_eventq *evq, */ if (in_isr()) { + if (!npl_eventq_queued_get_isr(event)) { + return; + } + + removed = false; woken = pdFALSE; + portENTER_CRITICAL_ISR(&ble_port_mutex); count = uxQueueMessagesWaitingFromISR(eventq->q); for (i = 0; i < count; i++) { ret = xQueueReceiveFromISR(eventq->q, &tmp_ev, &woken2); - BLE_LL_ASSERT(ret == pdPASS); + if (ret != pdPASS) { + break; + } woken |= woken2; if (tmp_ev == ev) { + removed = true; continue; } ret = xQueueSendToBackFromISR(eventq->q, &tmp_ev, &woken2); - BLE_LL_ASSERT(ret == pdPASS); + if (ret != pdPASS) { + npl_eventq_lost_event_clear(tmp_ev); + break; + } woken |= woken2; } + if (removed) { + event->queued = false; + } + portEXIT_CRITICAL_ISR(&ble_port_mutex); if( woken == pdTRUE ) { portYIELD_FROM_ISR(); } } else { - portENTER_CRITICAL(&ble_port_mutex); + removed = false; + bool locked = npl_eventq_lock(); + + if (!npl_eventq_queued_get_task(event)) { + npl_eventq_unlock(locked); + return; + } + + portENTER_CRITICAL(&ble_port_mutex); count = uxQueueMessagesWaiting(eventq->q); for (i = 0; i < count; i++) { ret = xQueueReceive(eventq->q, &tmp_ev, 0); - BLE_LL_ASSERT(ret == pdPASS); + if (ret != pdPASS) { + break; + } if (tmp_ev == ev) { + removed = true; continue; } ret = xQueueSendToBack(eventq->q, &tmp_ev, 0); - BLE_LL_ASSERT(ret == pdPASS); + if (ret != pdPASS) { + npl_eventq_lost_event_clear(tmp_ev); + break; + } + } + if (removed) { + event->queued = false; } - portEXIT_CRITICAL(&ble_port_mutex); + npl_eventq_unlock(locked); } - - event->queued = 0; } ble_npl_error_t @@ -833,9 +1057,19 @@ IRAM_ATTR npl_freertos_callout_stop(struct ble_npl_callout *co) } #if BLE_NPL_USE_ESP_TIMER - esp_timer_stop(callout->handle); + if (!in_isr()) { + esp_timer_stop(callout->handle); + } #else - xTimerStop(callout->handle, portMAX_DELAY); + 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) { @@ -1019,26 +1253,40 @@ IRAM_ATTR npl_freertos_time_delay(ble_npl_time_t ticks) } -uint8_t hw_critical_state_status = 0; - uint32_t IRAM_ATTR npl_freertos_hw_enter_critical(void) { - ++hw_critical_state_status; + BaseType_t core; + portENTER_CRITICAL(&ble_port_mutex); + core = xPortGetCoreID(); + if (core < portNUM_PROCESSORS) { + ++hw_critical_state_status[core]; + } return 0; } uint8_t IRAM_ATTR npl_freertos_hw_is_in_critical(void) { - return hw_critical_state_status; + BaseType_t core; + + core = xPortGetCoreID(); + if (core >= portNUM_PROCESSORS) { + return 0; + } + return hw_critical_state_status[core]; } void IRAM_ATTR npl_freertos_hw_exit_critical(uint32_t ctx) { - --hw_critical_state_status; + BaseType_t core; + + core = xPortGetCoreID(); + if (core < portNUM_PROCESSORS && hw_critical_state_status[core] > 0) { + --hw_critical_state_status[core]; + } portEXIT_CRITICAL(&ble_port_mutex); } @@ -1129,6 +1377,9 @@ int npl_freertos_set_controller_npl_info(ble_npl_count_info_t *ctrl_npl_info) int npl_freertos_mempool_init(void) { int rc = -1; + + npl_eventq_sync_init(); + uint16_t ble_total_evt_count = 0; uint16_t ble_total_co_count = 0; uint16_t ble_total_evtq_count = 0; @@ -1218,6 +1469,11 @@ int npl_freertos_mempool_init(void) return 0; _error: + if (npl_eventq_sync) { + vSemaphoreDelete(npl_eventq_sync); + npl_eventq_sync = NULL; + } + if (ble_freertos_ev_buf) { bt_osi_mem_free_internal(ble_freertos_ev_buf); ble_freertos_ev_buf = NULL; @@ -1247,6 +1503,11 @@ _error: void npl_freertos_mempool_deinit(void) { + if (npl_eventq_sync) { + vSemaphoreDelete(npl_eventq_sync); + npl_eventq_sync = NULL; + } + if (ble_freertos_ev_buf) { bt_osi_mem_free_internal(ble_freertos_ev_buf); ble_freertos_ev_buf = NULL; diff --git a/components/bt/porting_btdm/controller/btdm_common/src/btdm_osal_freertos.c b/components/bt/porting_btdm/controller/btdm_common/src/btdm_osal_freertos.c index b9220767d18..1efa66bdf09 100644 --- a/components/bt/porting_btdm/controller/btdm_common/src/btdm_osal_freertos.c +++ b/components/bt/porting_btdm/controller/btdm_common/src/btdm_osal_freertos.c @@ -353,7 +353,10 @@ bool IRAM_ATTR wr_btdm_osal_event_is_queued(struct btdm_osal_event *ev) { struct btdm_osal_event_freertos *event = (struct btdm_osal_event_freertos *)ev->event; - return event->queued; + if (event) { + return event->queued; + } + return false; } void *IRAM_ATTR 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 5bd85f57ab8..e6e2323fb75 100644 --- a/components/bt/test_apps/basic_unit_test/main/CMakeLists.txt +++ b/components/bt/test_apps/basic_unit_test/main/CMakeLists.txt @@ -2,6 +2,8 @@ idf_component_register(SRCS "test_bt_main.c" "test_bt_common.c" "test_smp.c" "test_tinycrypt_ecc.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 diff --git a/components/protocomm/src/simple_ble/simple_ble.c b/components/protocomm/src/simple_ble/simple_ble.c index b1a9fdcbfd6..1d77493a3bc 100644 --- a/components/protocomm/src/simple_ble/simple_ble.c +++ b/components/protocomm/src/simple_ble/simple_ble.c @@ -159,17 +159,17 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ adv_config_done |= scan_rsp_config_flag; break; case ESP_GATTS_READ_EVT: - if (g_ble_cfg_p) { + if (g_ble_cfg_p && g_ble_cfg_p->read_fn) { g_ble_cfg_p->read_fn(event, gatts_if, param); } break; case ESP_GATTS_WRITE_EVT: - if (g_ble_cfg_p) { + if (g_ble_cfg_p && g_ble_cfg_p->write_fn) { g_ble_cfg_p->write_fn(event, gatts_if, param); } break; case ESP_GATTS_EXEC_WRITE_EVT: - if (g_ble_cfg_p) { + if (g_ble_cfg_p && g_ble_cfg_p->exec_write_fn) { g_ble_cfg_p->exec_write_fn(event, gatts_if, param); } break; @@ -187,7 +187,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ break; case ESP_GATTS_CONNECT_EVT: ESP_LOGD(TAG, "ESP_GATTS_CONNECT_EVT, conn_id = %d", param->connect.conn_id); - if (g_ble_cfg_p) { + if (g_ble_cfg_p && g_ble_cfg_p->connect_fn) { g_ble_cfg_p->connect_fn(event, gatts_if, param); } esp_ble_conn_update_params_t conn_params = {0}; @@ -202,7 +202,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ break; case ESP_GATTS_DISCONNECT_EVT: ESP_LOGD(TAG, "ESP_GATTS_DISCONNECT_EVT, reason = %d", param->disconnect.reason); - if (g_ble_cfg_p) { + if (g_ble_cfg_p && g_ble_cfg_p->disconnect_fn) { g_ble_cfg_p->disconnect_fn(event, gatts_if, param); } memset(s_cached_remote_bda, 0, sizeof(esp_bd_addr_t)); @@ -263,7 +263,6 @@ esp_err_t simple_ble_deinit(void) simple_ble_cfg_t *ble_cfg = g_ble_cfg_p; g_ble_cfg_p = NULL; if (ble_cfg) { - free(ble_cfg->gatt_db); ble_cfg->gatt_db = NULL; free(ble_cfg); } diff --git a/components/protocomm/src/transports/protocomm_ble.c b/components/protocomm/src/transports/protocomm_ble.c index a64fd1d84a8..179c38c679c 100644 --- a/components/protocomm/src/transports/protocomm_ble.c +++ b/components/protocomm/src/transports/protocomm_ble.c @@ -74,6 +74,7 @@ typedef struct _protocomm_ble { } _protocomm_ble_internal_t; static _protocomm_ble_internal_t *protoble_internal; +static esp_gatts_attr_db_t *s_gatt_db; static bool protocomm_ble_transport_active(void) { @@ -715,6 +716,8 @@ static ssize_t populate_gatt_db(esp_gatts_attr_db_t **gatt_db_generated) static void protocomm_ble_cleanup(void) { protocomm_ble_reset_prepare_write(); + free(s_gatt_db); + s_gatt_db = NULL; if (protoble_internal) { if (protoble_internal->service_uuid) { free(protoble_internal->service_uuid); @@ -891,6 +894,7 @@ esp_err_t protocomm_ble_start(protocomm_t *pc, const protocomm_ble_config_t *con ble_config->device_name = protocomm_ble_device_name; ble_config->gatt_db_count = populate_gatt_db(&ble_config->gatt_db); + s_gatt_db = ble_config->gatt_db; ble_config->ble_bonding = config->ble_bonding; ble_config->ble_sm_sc = config->ble_sm_sc; @@ -904,7 +908,9 @@ esp_err_t protocomm_ble_start(protocomm_t *pc, const protocomm_ble_config_t *con if (ble_config->gatt_db_count == -1) { ESP_LOGE(TAG, "Invalid GATT database count"); - free(ble_config->gatt_db); + free(s_gatt_db); + s_gatt_db = NULL; + ble_config->gatt_db = NULL; free(ble_config); protocomm_ble_cleanup(); return ESP_ERR_INVALID_STATE; diff --git a/examples/bluetooth/.build-test-rules.yml b/examples/bluetooth/.build-test-rules.yml index 5e3b3118106..f8512d4bd7b 100644 --- a/examples/bluetooth/.build-test-rules.yml +++ b/examples/bluetooth/.build-test-rules.yml @@ -466,14 +466,10 @@ examples/bluetooth/nimble/throughput_app: <<: *bt_default_depends disable: - if: SOC_BLE_SUPPORTED != 1 - depends_components+: - - esp_driver_gpio - - esp_driver_uart depends_filepatterns: - examples/bluetooth/nimble/common/**/* - - examples/bluetooth/nimble/throughput_app/blecent_throughput/components/**/* -examples/bluetooth/nimble/throughput_app/blecent_throughput: +examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput: <<: *bt_default_depends disable: - if: SOC_BLE_SUPPORTED != 1 @@ -482,4 +478,20 @@ examples/bluetooth/nimble/throughput_app/blecent_throughput: - esp_driver_uart depends_filepatterns: - examples/bluetooth/nimble/common/**/* - - examples/bluetooth/nimble/throughput_app/blecent_throughput/components/**/* + - examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/**/* + +examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent: + <<: *bt_default_depends + disable: + - if: SOC_BLE_SUPPORTED != 1 + depends_filepatterns: + - examples/bluetooth/nimble/common/**/* + - examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/**/* + +examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph: + <<: *bt_default_depends + disable: + - if: SOC_BLE_SUPPORTED != 1 + depends_filepatterns: + - examples/bluetooth/nimble/common/**/* + - examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/**/* diff --git a/examples/bluetooth/nimble/ble_chan_sound_reflector/main/main.c b/examples/bluetooth/nimble/ble_chan_sound_reflector/main/main.c index 3c0d6f2f241..d367d3fbb66 100644 --- a/examples/bluetooth/nimble/ble_chan_sound_reflector/main/main.c +++ b/examples/bluetooth/nimble/ble_chan_sound_reflector/main/main.c @@ -367,7 +367,7 @@ bleprph_gap_event(struct ble_gap_event *event, void *arg) rc = ble_gap_conn_find(event->enc_change.conn_handle, &desc); assert(rc == 0); bleprph_print_conn_desc(&desc); - struct ble_cs_reflector_setup_params params; + struct ble_cs_reflector_setup_params params = {0}; params.cb=blecs_gap_event; ble_cs_reflector_setup(¶ms); diff --git a/examples/bluetooth/nimble/ble_l2cap_coc/coc_blecent/main/main.c b/examples/bluetooth/nimble/ble_l2cap_coc/coc_blecent/main/main.c index e6cff455f2f..9b375ce9357 100644 --- a/examples/bluetooth/nimble/ble_l2cap_coc/coc_blecent/main/main.c +++ b/examples/bluetooth/nimble/ble_l2cap_coc/coc_blecent/main/main.c @@ -97,7 +97,7 @@ blecent_l2cap_coc_send_data(struct ble_l2cap_chan *chan) static void blecent_l2cap_coc_on_disc_complete(const struct peer *peer, int status, void *arg) { - uint16_t psm = 0x1002; + uint16_t psm = 0x0080; struct os_mbuf *sdu_rx = NULL; int rc; diff --git a/examples/bluetooth/nimble/ble_l2cap_coc/coc_bleprph/main/main.c b/examples/bluetooth/nimble/ble_l2cap_coc/coc_bleprph/main/main.c index a8e5f3b8df0..b40f89b38b0 100644 --- a/examples/bluetooth/nimble/ble_l2cap_coc/coc_bleprph/main/main.c +++ b/examples/bluetooth/nimble/ble_l2cap_coc/coc_bleprph/main/main.c @@ -34,7 +34,7 @@ void ble_store_config_init(void); #define COC_BUF_COUNT (20 * MYNEWT_VAL(BLE_L2CAP_COC_MAX_NUM)) #define MTU 512 -uint16_t psm = 0x1002; +uint16_t psm = 0x0080; static os_membuf_t sdu_coc_mem[OS_MEMPOOL_SIZE(COC_BUF_COUNT, MTU)]; static struct os_mempool sdu_coc_mbuf_mempool; static struct os_mbuf_pool sdu_os_mbuf_pool; @@ -332,16 +332,6 @@ bleprph_gap_event(struct ble_gap_event *event, void *arg) ext_bleprph_advertise(); #else bleprph_advertise(); -#endif - } else { - rc = ble_gap_conn_find(event->connect.conn_handle, &desc); - assert(rc == 0); - bleprph_print_conn_desc(&desc); -#if MYNEWT_VAL(BLE_L2CAP_COC_MAX_NUM) >= 1 - rc = ble_l2cap_create_server(psm, MTU, bleprph_l2cap_coc_event_cb, NULL); - if (rc != 0) { - MODLOG_DFLT(ERROR, "Failed to create L2CAP CoC server; rc=%d", rc); - } #endif } return 0; @@ -413,6 +403,15 @@ bleprph_on_sync(void) MODLOG_DFLT(INFO, "Device Address: "); print_addr(addr_val); MODLOG_DFLT(INFO, "\n"); + +#if MYNEWT_VAL(BLE_L2CAP_COC_MAX_NUM) >= 1 + rc = ble_l2cap_create_server(psm, MTU, bleprph_l2cap_coc_event_cb, NULL); + if (rc != 0 && rc != BLE_HS_EALREADY) { + MODLOG_DFLT(ERROR, "Failed to create L2CAP COC server; rc=%d\n", rc); + return; + } +#endif + /* Begin advertising. */ #if CONFIG_EXAMPLE_EXTENDED_ADV ext_bleprph_advertise(); diff --git a/examples/bluetooth/nimble/ble_pawr_adv_conn/ble_pawr_adv_conn/main/main.c b/examples/bluetooth/nimble/ble_pawr_adv_conn/ble_pawr_adv_conn/main/main.c index d5a6fe0fc8f..03f9dab3db5 100644 --- a/examples/bluetooth/nimble/ble_pawr_adv_conn/ble_pawr_adv_conn/main/main.c +++ b/examples/bluetooth/nimble/ble_pawr_adv_conn/ble_pawr_adv_conn/main/main.c @@ -18,6 +18,10 @@ #define BLE_PAWR_RSP_SLOT_SPACING (10) /*!< Time between response slots (N * 0.125 ms) */ #define BLE_PAWR_NUM_RSP_SLOTS (25) /*!< Number of subevent response slots */ #define BLE_PAWR_SUB_DATA_LEN (20) +/* Give the controller a few periodic intervals to report the outcome of a + * synchronized connection attempt before retrying from another subevent. + */ +#define BLE_PAWR_CONN_TIMEOUT_MS (3 * BLE_PAWR_EVENT_PERIODIC_INTERVAL_MS) #define TAG "NimBLE_BLE_PAwR_CONN" @@ -158,7 +162,7 @@ gap_event_cb(struct ble_gap_event *event, void *arg) phy_mask = 0x01; if (conn == 0) { - rc = ble_gap_connect_with_synced(own_addr_type,adv_handle,subevent,&peer_addr,30000,phy_mask,NULL,NULL,NULL,gap_event_cb,NULL); + rc = ble_gap_connect_with_synced(own_addr_type,adv_handle,subevent,&peer_addr,BLE_PAWR_CONN_TIMEOUT_MS,phy_mask,NULL,NULL,NULL,gap_event_cb,NULL); if (rc != 0 ) { ESP_LOGI(TAG,"Error: Failed to connect to device , rc = %d\n",rc); }else { diff --git a/examples/bluetooth/nimble/throughput_app/README.md b/examples/bluetooth/nimble/throughput_app/README.md index f19b438111b..4e209860bde 100644 --- a/examples/bluetooth/nimble/throughput_app/README.md +++ b/examples/bluetooth/nimble/throughput_app/README.md @@ -1,22 +1,38 @@ # Throughput Demo Examples -There are two example folders inside this `throughput_app`: `bleprph_throughput` (peripheral) and `blecent_throughput` (central). These examples demonstrate BLE GATT throughput measurement using NimBLE on ESP32. Two ESP32 boards are needed to run this demo. The `blecent_throughput` example has CLI support to select GATT operation from READ/WRITE/NOTIFY and configure connection parameters at runtime. More details can be found in respective READMEs. +This folder contains BLE throughput measurement examples for NimBLE on ESP32, organized into two sub-folders by protocol: -## Using the Examples +``` +throughput_app/ +├── gatt/ +│ ├── blecent_throughput/ — GATT central (initiator) +│ └── bleprph_throughput/ — GATT peripheral (responder) +└── l2cap_coc/ + ├── l2cap_coc_cent/ — L2CAP CoC central (sender) + └── l2cap_coc_prph/ — L2CAP CoC peripheral (receiver) +``` + +--- + +## gatt/ + +There are two example folders inside `gatt/`: `bleprph_throughput` (peripheral) and `blecent_throughput` (central). These examples demonstrate BLE GATT throughput measurement using NimBLE on ESP32. Two ESP32 boards are needed to run this demo. The `blecent_throughput` example has CLI support to select GATT operation from READ/WRITE/NOTIFY and configure connection parameters at runtime. More details can be found in respective READMEs. + +### Using the Examples Build and flash two ESP32 boards with `bleprph_throughput` and `blecent_throughput` examples. The central automatically scans and connects to the peripheral based on device name (`nimble_prph`). After connection, the user may optionally configure connection parameters (`MTU`, `connection interval`, `latency`, `supervision timeout`, `connection event length`). Then the user specifies the throughput test type (`read`, `write` or `notify`) and test duration in seconds. Below are sample throughput numbers for a 60-second test run (MTU = 512, conn itvl = 7.5ms, DLE = 251 bytes, 1M PHY): |GATT Method | Measurement Time | Application Throughput| -|--- | --- | ---| -|NOTIFY | 60 seconds | ~340 Kbps| -|READ | 60 seconds | ~200 Kbps| -|WRITE | 60 seconds | ~500 Kbps| +|----------- | ---------------- | ----------------------| +|NOTIFY | 60 seconds | ~340 Kbps | +|READ | 60 seconds | ~200 Kbps | +|WRITE | 60 seconds | ~500 Kbps | The notify throughput output is displayed on the `bleprph_throughput` console, while read/write throughput results are shown on the `blecent_throughput` console. -## Throughput Optimization +### Throughput Optimization The following parameters have the most significant impact on throughput: @@ -35,3 +51,29 @@ The following parameters have the most significant impact on throughput: 7. **MSYS Buffer Count**: Both peripheral and central are configured with 50 MSYS blocks (`CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=50`) to provide sufficient buffer space for high-throughput operations. 8. **PHY**: On BLE 5.0 supported chipsets, 2M PHY can be selected to double the air data rate. Use Extended Advertising mode and specify PHY in the throughput CLI command. + +--- + +## l2cap_coc/ + +There are two example folders inside `l2cap_coc/`: `l2cap_coc_prph` (peripheral/receiver) and `l2cap_coc_cent` (central/sender). These examples demonstrate BLE L2CAP Connection-Oriented Channel (CoC) throughput measurement using NimBLE on ESP32. Two ESP32 boards are needed to run this demo. More details can be found in respective READMEs. + +### How It Works + +L2CAP CoC provides a direct channel between two devices without the ATT/GATT overhead, making it more efficient for bulk data transfer. + +- The peripheral (`l2cap_coc_prph`) advertises with UUID 0x1812 and registers an L2CAP CoC server on PSM 0x1002. On connection it pre-grants receive credits to the central so the central can pipeline multiple SDUs immediately. +- The central (`l2cap_coc_cent`) scans for UUID 0x1812, connects, enables Data Length Extension (DLE), then opens an L2CAP CoC channel and continuously sends SDUs to the peripheral. +- Data flows **central → peripheral**. The central controls PHY selection, cycling through all enabled PHYs (1M, 2M, Coded S2, Coded S8) in sequence and printing a TX throughput summary after each test interval. +- The peripheral tracks RX throughput per PHY, printing a per-PHY summary box each time the central switches PHY, and a live per-second RX rate while data is flowing. + +### Using the Examples + +Build and flash two ESP32 boards with `l2cap_coc_prph` and `l2cap_coc_cent` examples. The central automatically scans and connects — no user input required. The test runs continuously, cycling through enabled PHYs. + +Below are sample throughput numbers (MTU = 2048, DLE = 251 bytes, conn itvl = 7.5ms, ESP32-C6): + +| PHY | Measurement Time | Application Throughput | +|-----|-----------------|------------------------| +| 1M | 8 seconds | ~741 kbps | +| 2M | 8 seconds | ~1310 kbps | diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/CMakeLists.txt b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/CMakeLists.txt similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/CMakeLists.txt rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/CMakeLists.txt diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/README.md b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/README.md similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/README.md rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/README.md diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/components/cmd_system/CMakeLists.txt b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/cmd_system/CMakeLists.txt similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/components/cmd_system/CMakeLists.txt rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/cmd_system/CMakeLists.txt diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/components/cmd_system/cmd_system.c b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/cmd_system/cmd_system.c similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/components/cmd_system/cmd_system.c rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/cmd_system/cmd_system.c diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/components/cmd_system/cmd_system.h b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/cmd_system/cmd_system.h similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/components/cmd_system/cmd_system.h rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/cmd_system/cmd_system.h diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/components/cmd_system/component.mk b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/cmd_system/component.mk similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/components/cmd_system/component.mk rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/cmd_system/component.mk diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/main/CMakeLists.txt b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/CMakeLists.txt similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/main/CMakeLists.txt rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/CMakeLists.txt diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/main/Kconfig.projbuild b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/Kconfig.projbuild similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/main/Kconfig.projbuild rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/Kconfig.projbuild diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/main/gattc.h b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/gattc.h similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/main/gattc.h rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/gattc.h diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/main/main.c b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/main.c similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/main/main.c rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/main.c diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/main/misc.c b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/misc.c similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/main/misc.c rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/misc.c diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/main/peer.c b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/peer.c similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/main/peer.c rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/peer.c diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/main/scli.c b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/scli.c similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/main/scli.c rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/scli.c diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/sdkconfig.defaults b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults similarity index 91% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/sdkconfig.defaults rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults index c272b4700f4..e65b4a19cb7 100644 --- a/examples/bluetooth/nimble/throughput_app/blecent_throughput/sdkconfig.defaults +++ b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults @@ -5,9 +5,6 @@ # BT config (universal across all ESP32 variants) # CONFIG_BT_ENABLED=y -CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y -CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n -CONFIG_BTDM_CTRL_MODE_BTDM=n CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y diff --git a/examples/bluetooth/nimble/throughput_app/blecent_throughput/sdkconfig.defaults.esp32c6 b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults.esp32c6 similarity index 100% rename from examples/bluetooth/nimble/throughput_app/blecent_throughput/sdkconfig.defaults.esp32c6 rename to examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults.esp32c6 diff --git a/examples/bluetooth/nimble/throughput_app/bleprph_throughput/CMakeLists.txt b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/CMakeLists.txt similarity index 100% rename from examples/bluetooth/nimble/throughput_app/bleprph_throughput/CMakeLists.txt rename to examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/CMakeLists.txt diff --git a/examples/bluetooth/nimble/throughput_app/bleprph_throughput/README.md b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/README.md similarity index 100% rename from examples/bluetooth/nimble/throughput_app/bleprph_throughput/README.md rename to examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/README.md diff --git a/examples/bluetooth/nimble/throughput_app/bleprph_throughput/main/CMakeLists.txt b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/CMakeLists.txt similarity index 100% rename from examples/bluetooth/nimble/throughput_app/bleprph_throughput/main/CMakeLists.txt rename to examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/CMakeLists.txt diff --git a/examples/bluetooth/nimble/throughput_app/bleprph_throughput/main/Kconfig.projbuild b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/Kconfig.projbuild similarity index 100% rename from examples/bluetooth/nimble/throughput_app/bleprph_throughput/main/Kconfig.projbuild rename to examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/Kconfig.projbuild diff --git a/examples/bluetooth/nimble/throughput_app/bleprph_throughput/main/gatt_svr.c b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/gatt_svr.c similarity index 100% rename from examples/bluetooth/nimble/throughput_app/bleprph_throughput/main/gatt_svr.c rename to examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/gatt_svr.c diff --git a/examples/bluetooth/nimble/throughput_app/bleprph_throughput/main/gatts_sens.h b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/gatts_sens.h similarity index 100% rename from examples/bluetooth/nimble/throughput_app/bleprph_throughput/main/gatts_sens.h rename to examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/gatts_sens.h diff --git a/examples/bluetooth/nimble/throughput_app/bleprph_throughput/main/main.c b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/main.c similarity index 100% rename from examples/bluetooth/nimble/throughput_app/bleprph_throughput/main/main.c rename to examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/main.c diff --git a/examples/bluetooth/nimble/throughput_app/bleprph_throughput/sdkconfig.defaults b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults similarity index 90% rename from examples/bluetooth/nimble/throughput_app/bleprph_throughput/sdkconfig.defaults rename to examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults index dcd2fab65be..2b58c85d2ff 100644 --- a/examples/bluetooth/nimble/throughput_app/bleprph_throughput/sdkconfig.defaults +++ b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults @@ -5,9 +5,6 @@ # BT config (universal across all ESP32 variants) # CONFIG_BT_ENABLED=y -CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y -CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n -CONFIG_BTDM_CTRL_MODE_BTDM=n CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y diff --git a/examples/bluetooth/nimble/throughput_app/bleprph_throughput/sdkconfig.defaults.esp32c6 b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults.esp32c6 similarity index 100% rename from examples/bluetooth/nimble/throughput_app/bleprph_throughput/sdkconfig.defaults.esp32c6 rename to examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults.esp32c6 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/CMakeLists.txt b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/CMakeLists.txt new file mode 100644 index 00000000000..c66bff776f7 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(l2cap_coc_cent) diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/README.md b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/README.md new file mode 100644 index 00000000000..01910368412 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/README.md @@ -0,0 +1,94 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-S3 | ESP32-S31 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | --------- | + +# L2CAP COC Throughput Central Example + +`l2cap_coc_cent` demonstrates the central (initiator) side of an L2CAP Connection-Oriented Channel (COC) throughput test using NimBLE on ESP32. It passively scans for a peripheral advertising UUID 0x1812, establishes a GAP connection, enables Data Length Extension (DLE), then opens an L2CAP COC channel over PSM 0x1002 and continuously sends SDUs to measure TX throughput. + +The central automatically cycles through all enabled PHYs (1M, 2M, Coded S2, Coded S8) in sequence, printing a throughput summary box after each test interval. It must be used together with the `l2cap_coc_prph` example which acts as the receiving side. + +It uses ESP32's Bluetooth controller and NimBLE stack based BLE host. + +## How to Use Example + +Before project configuration and build, be sure to set the correct chip target using: + +```bash +idf.py set-target +``` + +### Hardware Required + +* Two development boards, one flashed with `l2cap_coc_cent` and the other with `l2cap_coc_prph`. +* A USB cable for power supply and programming. + +See [Development Boards](https://www.espressif.com/en/products/devkits) for more information. + +### Configure the Project + +Open the project configuration menu: + +```bash +idf.py menuconfig +``` + +In the `L2CAP COC Throughput Configuration` menu: + +| Option | Default | Description | +|--------|---------|-------------| +| `EXAMPLE_L2CAP_COC_MTU` | 2048 | L2CAP CoC SDU MTU size in bytes (central receive buffer). Data flows cent → prph in this test, so throughput is governed by the peripheral's MTU. This value only limits how much the peripheral can send back and does not affect TX throughput. | +| `EXAMPLE_EXTENDED_ADV` | y (BLE 5.0 chips) | Enable extended scanning to find peripherals using extended advertising. Required for Coded PHY testing on ESP32-C6/H2. | +| `EXAMPLE_TEST_PHY_1M` | n | Enable throughput test on 1M PHY. | +| `EXAMPLE_TEST_PHY_2M` | y | Enable throughput test on 2M PHY (BLE 5.0 chips only). | +| `EXAMPLE_TEST_PHY_CODED_S2` | n | Enable throughput test on Coded PHY S2 (500 kbps, BLE 5.0 chips only). | +| `EXAMPLE_TEST_PHY_CODED_S8` | n | Enable throughput test on Coded PHY S8 (125 kbps, BLE 5.0 chips only). | +| `EXAMPLE_TEST_DURATION_1M` | 8 | Test duration in seconds for 1M PHY. | +| `EXAMPLE_TEST_DURATION_2M` | 8 | Test duration in seconds for 2M PHY. | +| `EXAMPLE_TEST_DURATION_CODED_S2` | 8 | Test duration in seconds for Coded S2 PHY. | +| `EXAMPLE_TEST_DURATION_CODED_S8` | 8 | Test duration in seconds for Coded S8 PHY. | + +### Build and Flash + +Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +On successful connection and throughput test, the central prints a per-PHY summary box after each test interval, then loops back to the first enabled PHY continuously: + +``` +I (xxx) l2cap_coc_cent: BLE Host Task started +I (xxx) l2cap_coc_cent: Device Address: xx:xx:xx:xx:xx:xx +I (xxx) l2cap_coc_cent: Connecting to xx:xx:xx:xx:xx:xx (addr_type=0) +I (xxx) l2cap_coc_cent: Connected; handle=0 peer=xx:xx:xx:xx:xx:xx +I (xxx) l2cap_coc_cent: L2CAP COC connected, chan=0xxxxxxxxx +I (xxx) l2cap_coc_cent: L2CAP COC Throughput — TX side (central sends to peripheral) +I (xxx) l2cap_coc_cent: Number of enabled PHYs: x +I (xxx) l2cap_coc_cent: PHY updated: tx=2 rx=2 status=0 +I (xxx) l2cap_coc_cent: [2M PHY] Sending for 8 s +I (xxx) l2cap_coc_cent: +-------------------------------------------------+ +I (xxx) l2cap_coc_cent: | PHY : 2M | +I (xxx) l2cap_coc_cent: | TX : xxxx kbps | +I (xxx) l2cap_coc_cent: | Bytes : xxxxxxx | +I (xxx) l2cap_coc_cent: | Time : 8 s | +I (xxx) l2cap_coc_cent: +-------------------------------------------------+ +I (xxx) l2cap_coc_cent: Cycle complete. Looping back to first PHY... +I (xxx) l2cap_coc_cent: PHY updated: tx=2 rx=2 status=0 +I (xxx) l2cap_coc_cent: [2M PHY] Sending for 8 s +I (xxx) l2cap_coc_cent: +-------------------------------------------------+ +I (xxx) l2cap_coc_cent: | PHY : 2M | +I (xxx) l2cap_coc_cent: | TX : xxxx kbps | +I (xxx) l2cap_coc_cent: | Bytes : xxxxxxx | +I (xxx) l2cap_coc_cent: | Time : 8 s | +I (xxx) l2cap_coc_cent: +-------------------------------------------------+ +I (xxx) l2cap_coc_cent: Cycle complete. Looping back to first PHY... +``` + +> **Note:** The above output was captured on ESP32-H2 with only 2M PHY enabled. With additional PHYs enabled (1M, Coded S2, Coded S8), the central cycles through each in sequence before looping back. + +## Troubleshooting + +For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/CMakeLists.txt b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/CMakeLists.txt new file mode 100644 index 00000000000..718f89cab8e --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "main.c" + PRIV_REQUIRES bt nvs_flash esp_timer + INCLUDE_DIRS ".") diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/Kconfig.projbuild b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/Kconfig.projbuild new file mode 100644 index 00000000000..5632e2c99d0 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/Kconfig.projbuild @@ -0,0 +1,67 @@ +menu "L2CAP COC Throughput Configuration" + + config EXAMPLE_L2CAP_COC_MTU + int "L2CAP CoC MTU size in bytes" + default 2048 + range 512 65511 + help + L2CAP CoC SDU MTU size used by the central device. + Note: memory pool allocates 6 buffers of this size; total pool + memory = MTU x 6. On chips without PSRAM ensure sufficient heap + is available before increasing this value. + Use idf.py size-components to verify. + + config EXAMPLE_EXTENDED_ADV + bool + depends on SOC_BLE_50_SUPPORTED && BT_NIMBLE_50_FEATURE_SUPPORT + default y if SOC_ESP_NIMBLE_CONTROLLER + select BT_NIMBLE_EXT_ADV + prompt "Enable Extended Scanning" + help + Use extended scanning on chips that support BLE 5.0 + + config EXAMPLE_TEST_PHY_1M + bool "Test on 1M PHY" + default y if !SOC_BLE_50_SUPPORTED + default n + + config EXAMPLE_TEST_PHY_2M + bool "Test on 2M PHY" + default y + depends on SOC_BLE_50_SUPPORTED + + config EXAMPLE_TEST_PHY_CODED_S2 + bool "Test on Coded PHY S2" + default n + depends on SOC_BLE_50_SUPPORTED + + config EXAMPLE_TEST_PHY_CODED_S8 + bool "Test on Coded PHY S8" + default n + depends on SOC_BLE_50_SUPPORTED + + config EXAMPLE_TEST_DURATION_1M + int "Test duration for 1M PHY (sec)" + default 8 + range 1 3600 + depends on EXAMPLE_TEST_PHY_1M + + config EXAMPLE_TEST_DURATION_2M + int "Test duration for 2M PHY (sec)" + default 8 + range 1 3600 + depends on EXAMPLE_TEST_PHY_2M + + config EXAMPLE_TEST_DURATION_CODED_S2 + int "Test duration for Coded S2 PHY (sec)" + default 8 + range 1 3600 + depends on EXAMPLE_TEST_PHY_CODED_S2 + + config EXAMPLE_TEST_DURATION_CODED_S8 + int "Test duration for Coded S8 PHY (sec)" + default 8 + range 1 3600 + depends on EXAMPLE_TEST_PHY_CODED_S8 + +endmenu diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/idf_component.yml b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/idf_component.yml new file mode 100644 index 00000000000..db8886afea4 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + nimble_central_utils: + path: ${IDF_PATH}/examples/bluetooth/nimble/common/nimble_central_utils diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/main.c b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/main.c new file mode 100644 index 00000000000..09f2c989884 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/main/main.c @@ -0,0 +1,738 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "host/ble_hs.h" +#include "host/util/util.h" +#include "services/gap/ble_svc_gap.h" +#include "host/ble_esp_gap.h" + +static const char *TAG = "l2cap_coc_cent"; + +#define L2CAP_COC_PSM 0x1002 +#define L2CAP_COC_MTU CONFIG_EXAMPLE_L2CAP_COC_MTU +#define COC_BUF_COUNT (6 * MYNEWT_VAL(BLE_L2CAP_COC_MAX_NUM)) +/* Block size must include mbuf headers so each SDU fits in one pool entry. */ +#define SDU_BLOCK_SIZE (L2CAP_COC_MTU + sizeof(struct os_mbuf_pkthdr) + sizeof(struct os_mbuf)) +#define LL_PACKET_LENGTH 251 +#define LL_PACKET_TIME 2120 +#define L2CAP_COC_UUID 0x1812 + +/* EventGroup bits */ +#define PHY_UPDATED_BIT (1 << 0) +#define COC_CONNECTED_BIT (1 << 1) +#define CONN_UPDATED_BIT (1 << 2) +#define TX_UNSTALLED_BIT (1 << 3) + +/* Timeout / interval constants */ +#define CONN_PARAM_UPDATE_TIMEOUT_MS 5000 +#define PREDRAIN_TIMEOUT_MS 20000 +#define POSTDRAIN_TIMEOUT_MS 5000 +#define TX_YIELD_INTERVAL 50 + +static EventGroupHandle_t coc_event_group; +static uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE; +static struct ble_l2cap_chan *coc_chan = NULL; +static bool ci_is_slow = false; +static bool l2cap_connecting = false; /* guards double L2CAP connect */ +static volatile bool chan_stalled = false; +static uint32_t *cent_seg_tx_done = NULL; /* points to segment SDU counter for async TX_UNSTALLED */ +static uint32_t *cent_seg_tx_drop = NULL; /* counts SDUs dropped (TX_UNSTALLED status != 0) */ +static uint16_t cent_tx_sdu_len = L2CAP_COC_MTU; /* min(local, peer) after COC connect */ + +static const struct ble_gap_upd_params conn_params = { + .itvl_min = 6, + .itvl_max = 6, + .latency = 0, + .supervision_timeout = 2000, + .min_ce_len = 12, + .max_ce_len = 24, +}; + +void ble_store_config_init(void); + +static os_membuf_t sdu_coc_mem[OS_MEMPOOL_SIZE(COC_BUF_COUNT, SDU_BLOCK_SIZE)]; +static struct os_mempool sdu_coc_mempool; +static struct os_mbuf_pool sdu_os_mbuf_pool; + +typedef struct { + uint8_t tx_phys; + uint8_t rx_phys; + uint8_t phy_opts; /* 0=none, 1=S2, 2=S8 */ + int duration_s; + const char *name; + bool is_coded_s8; +} phy_entry_t; + +static const phy_entry_t phy_list[] = { +#if CONFIG_EXAMPLE_TEST_PHY_1M + { BLE_HCI_LE_PHY_1M_PREF_MASK, BLE_HCI_LE_PHY_1M_PREF_MASK, 0, + CONFIG_EXAMPLE_TEST_DURATION_1M, "1M", false }, +#endif +#if CONFIG_EXAMPLE_TEST_PHY_2M + { BLE_HCI_LE_PHY_2M_PREF_MASK, BLE_HCI_LE_PHY_2M_PREF_MASK, 0, + CONFIG_EXAMPLE_TEST_DURATION_2M, "2M", false }, +#endif +#if CONFIG_EXAMPLE_TEST_PHY_CODED_S2 + { BLE_HCI_LE_PHY_CODED_PREF_MASK, BLE_HCI_LE_PHY_CODED_PREF_MASK, 0x01, + CONFIG_EXAMPLE_TEST_DURATION_CODED_S2, "Coded S2", false }, +#endif +#if CONFIG_EXAMPLE_TEST_PHY_CODED_S8 + { BLE_HCI_LE_PHY_CODED_PREF_MASK, BLE_HCI_LE_PHY_CODED_PREF_MASK, 0x02, + CONFIG_EXAMPLE_TEST_DURATION_CODED_S8, "Coded S8", true }, +#endif +}; +#define PHY_LIST_LEN ((int)(sizeof(phy_list) / sizeof(phy_list[0]))) + +static int cent_gap_event(struct ble_gap_event *event, void *arg); +static int cent_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg); + +static void cent_l2cap_coc_mem_init(void) +{ + int rc; + rc = os_mempool_init(&sdu_coc_mempool, COC_BUF_COUNT, SDU_BLOCK_SIZE, + sdu_coc_mem, "cent_coc_pool"); + assert(rc == 0); + rc = os_mbuf_pool_init(&sdu_os_mbuf_pool, &sdu_coc_mempool, + SDU_BLOCK_SIZE, COC_BUF_COUNT); + assert(rc == 0); +} + +static void cent_l2cap_coc_connect(uint16_t conn_handle) +{ + struct ble_gap_conn_desc desc; + if (ble_gap_conn_find(conn_handle, &desc) != 0) { + ESP_LOGE(TAG, "L2CAP COC connect: connection %d not found", conn_handle); + l2cap_connecting = false; + return; + } + struct os_mbuf *sdu_rx = os_mbuf_get_pkthdr(&sdu_os_mbuf_pool, 0); + if (!sdu_rx) { + ESP_LOGE(TAG, "Failed to alloc sdu_rx for L2CAP connect"); + l2cap_connecting = false; + return; + } + int rc = ble_l2cap_connect(conn_handle, L2CAP_COC_PSM, L2CAP_COC_MTU, + sdu_rx, cent_l2cap_coc_event_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "L2CAP COC connect failed; rc=%d", rc); + l2cap_connecting = false; + /* EINVAL: NimBLE returns before chan alloc, sdu_rx not consumed — free it. + * ENOTCONN: NimBLE frees sdu_rx on all ENOTCONN paths (early !conn check + * and late TX failure via ble_l2cap_coc_cleanup_chan). Do not free here. */ + if (rc == BLE_HS_EINVAL) { + os_mbuf_free_chain(sdu_rx); + } + } +} + +static void cent_scan(void) +{ + struct ble_gap_disc_params disc_params = { + .filter_duplicates = 1, + .passive = 1, + }; + uint8_t own_addr_type; + int rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) { + ESP_LOGE(TAG, "Error inferring addr type; rc=%d", rc); + return; + } + rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params, + cent_gap_event, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Error starting scan; rc=%d", rc); + } +} + +static int cent_should_connect(const struct ble_gap_disc_desc *disc) +{ + struct ble_hs_adv_fields fields; + if (disc->event_type != BLE_HCI_ADV_RPT_EVTYPE_ADV_IND && + disc->event_type != BLE_HCI_ADV_RPT_EVTYPE_DIR_IND) { + return 0; + } + int rc = ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data); + if (rc != 0) { + return 0; + } + for (int i = 0; i < fields.num_uuids16; i++) { + if (ble_uuid_u16(&fields.uuids16[i].u) == L2CAP_COC_UUID) { + return 1; + } + } + return 0; +} + +static void cent_connect_if_interesting(const struct ble_gap_disc_desc *disc) +{ + if (!cent_should_connect(disc)) { + return; + } + int rc = ble_gap_disc_cancel(); + if (rc != 0) { + return; + } + uint8_t own_addr_type; + rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) { + ESP_LOGE(TAG, "Error inferring addr type; rc=%d", rc); + cent_scan(); + return; + } + ESP_LOGI(TAG, "Connecting to %02x:%02x:%02x:%02x:%02x:%02x (addr_type=%d)", + disc->addr.val[5], disc->addr.val[4], disc->addr.val[3], + disc->addr.val[2], disc->addr.val[1], disc->addr.val[0], + disc->addr.type); + rc = ble_gap_connect(own_addr_type, &disc->addr, 30000, NULL, + cent_gap_event, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Connect failed; rc=%d", rc); + cent_scan(); + } +} + +#if CONFIG_EXAMPLE_EXTENDED_ADV +static void cent_connect_if_interesting_ext(const struct ble_gap_ext_disc_desc *disc) +{ + if (!(disc->props & BLE_HCI_ADV_CONN_MASK)) { + return; + } + struct ble_hs_adv_fields fields; + if (ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data) != 0) { + return; + } + int found = 0; + for (int i = 0; i < fields.num_uuids16; i++) { + if (ble_uuid_u16(&fields.uuids16[i].u) == L2CAP_COC_UUID) { + found = 1; + break; + } + } + if (!found) { + return; + } + int rc = ble_gap_disc_cancel(); + if (rc != 0) { + return; + } + uint8_t own_addr_type; + rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) { + ESP_LOGE(TAG, "Error inferring addr type; rc=%d", rc); + cent_scan(); + return; + } + ESP_LOGI(TAG, "Connecting to %02x:%02x:%02x:%02x:%02x:%02x (addr_type=%d)", + disc->addr.val[5], disc->addr.val[4], disc->addr.val[3], + disc->addr.val[2], disc->addr.val[1], disc->addr.val[0], + disc->addr.type); + rc = ble_gap_connect(own_addr_type, &disc->addr, 30000, NULL, + cent_gap_event, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Connect failed; rc=%d", rc); + cent_scan(); + } +} +#endif /* CONFIG_EXAMPLE_EXTENDED_ADV */ + +static int cent_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg) +{ + switch (event->type) { + case BLE_L2CAP_EVENT_COC_CONNECTED: { + struct ble_l2cap_chan_info info; + uint16_t peer_mtu; + + if (event->connect.status != 0) { + ESP_LOGE(TAG, "L2CAP COC connect status: %d,terminating GAP to restart", event->connect.status); + l2cap_connecting = false; + ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); + return 0; + } + ESP_LOGI(TAG, "L2CAP COC connected, chan=%p", event->connect.chan); + coc_chan = event->connect.chan; + + peer_mtu = 0; + cent_tx_sdu_len = L2CAP_COC_MTU; + if (ble_l2cap_get_chan_info(coc_chan, &info) == 0) { + peer_mtu = info.peer_coc_mtu; + if (peer_mtu > 0 && peer_mtu < cent_tx_sdu_len) { + cent_tx_sdu_len = peer_mtu; + } + } + ESP_LOGI(TAG, "TX SDU size: %u bytes (peer CoC MTU %u)", + cent_tx_sdu_len, peer_mtu); + l2cap_connecting = false; + xEventGroupSetBits(coc_event_group, COC_CONNECTED_BIT); + return 0; + } + + case BLE_L2CAP_EVENT_COC_DISCONNECTED: + ESP_LOGI(TAG, "L2CAP COC disconnected"); + coc_chan = NULL; + chan_stalled = false; + cent_seg_tx_done = NULL; + cent_seg_tx_drop = NULL; + l2cap_connecting = false; + xEventGroupClearBits(coc_event_group, COC_CONNECTED_BIT); + xEventGroupSetBits(coc_event_group, TX_UNSTALLED_BIT | CONN_UPDATED_BIT | PHY_UPDATED_BIT); + return 0; + + case BLE_L2CAP_EVENT_COC_TX_UNSTALLED: + /* status==0: SDU delivered; status!=0: NimBLE dropped it (ENOMEM) — don't count. */ + chan_stalled = false; + if (event->tx_unstalled.status == 0) { + if (cent_seg_tx_done) { + (*cent_seg_tx_done)++; + } + } else { + if (cent_seg_tx_drop) { + (*cent_seg_tx_drop)++; + } + ESP_LOGD(TAG, "TX_UNSTALLED status=%d: SDU dropped", event->tx_unstalled.status); + } + xEventGroupSetBits(coc_event_group, TX_UNSTALLED_BIT); + return 0; + + case BLE_L2CAP_EVENT_COC_DATA_RECEIVED: { + struct os_mbuf *sdu_rx; + int rc; + + if (event->receive.sdu_rx) { + os_mbuf_free_chain(event->receive.sdu_rx); + } + sdu_rx = os_mbuf_get_pkthdr(&sdu_os_mbuf_pool, 0); + if (sdu_rx) { + rc = ble_l2cap_recv_ready(event->receive.chan, sdu_rx); + if (rc != 0) { + os_mbuf_free_chain(sdu_rx); + } + } else { + ESP_LOGE(TAG, "DATA_RECEIVED: no mbuf for recv_ready; RX may stall"); + } + return 0; + } + + default: + return 0; + } +} + +static void wait_unstall(uint32_t timeout_ms) +{ + xEventGroupWaitBits(coc_event_group, TX_UNSTALLED_BIT, pdTRUE, pdTRUE, pdMS_TO_TICKS(timeout_ms)); +} + +static void cent_send_task(void *arg) +{ + static uint8_t value[L2CAP_COC_MTU]; + int rc; + + for (int i = 0; i < L2CAP_COC_MTU; i++) { + value[i] = i & 0xFF; + } + + xEventGroupWaitBits(coc_event_group, COC_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY); + + ESP_LOGI(TAG, "L2CAP COC Throughput — TX side (central sends to peripheral)"); + ESP_LOGI(TAG, "Number of enabled PHYs: %d", (int)PHY_LIST_LEN); + + if (PHY_LIST_LEN == 0) { + ESP_LOGE(TAG, "No test PHY enabled; enable at least one EXAMPLE_TEST_PHY_* in menuconfig"); + vTaskDelete(NULL); + return; + } + + while (1) { + bool lost_connection = false; + + for (int i = 0; i < PHY_LIST_LEN && !lost_connection; i++) { + const phy_entry_t *phy = &phy_list[i]; + + xEventGroupClearBits(coc_event_group, PHY_UPDATED_BIT); +#if CONFIG_SOC_BLE_50_SUPPORTED + rc = ble_gap_set_prefered_le_phy(conn_handle, + phy->tx_phys, + phy->rx_phys, + phy->phy_opts); + if (rc != 0) { + ESP_LOGE(TAG, "PHY switch to %s failed; rc=%d — continuing anyway", + phy->name, rc); + xEventGroupSetBits(coc_event_group, PHY_UPDATED_BIT); + } +#else + xEventGroupSetBits(coc_event_group, PHY_UPDATED_BIT); +#endif + + EventBits_t bits = xEventGroupWaitBits(coc_event_group, PHY_UPDATED_BIT, + pdTRUE, pdTRUE, + pdMS_TO_TICKS(5000)); + if (!(bits & PHY_UPDATED_BIT)) { + ESP_LOGW(TAG, "PHY update timeout for %s; continuing anyway", phy->name); + } + + if (phy->is_coded_s8) { + /* CI=40ms, CE=32.5-40ms: fits 2 Coded S8 K-frames per CI and prevents credit starvation */ + struct ble_gap_upd_params s8_params = { + .itvl_min = 32, + .itvl_max = 32, + .latency = 0, + .supervision_timeout = 2000, + .min_ce_len = 52, + .max_ce_len = 64, + }; + xEventGroupClearBits(coc_event_group, CONN_UPDATED_BIT); + rc = ble_gap_update_params(conn_handle, &s8_params); + if (rc == 0) { + ESP_LOGI(TAG, "Coded S8: updating CI"); + ci_is_slow = true; + xEventGroupWaitBits(coc_event_group, CONN_UPDATED_BIT, pdTRUE, pdTRUE, + pdMS_TO_TICKS(CONN_PARAM_UPDATE_TIMEOUT_MS)); + } else { + ESP_LOGW(TAG, "S8 CI update failed (rc=%d)", rc); + } + } else if (phy->tx_phys == BLE_HCI_LE_PHY_CODED_PREF_MASK) { + /* CI=20ms, CE=10-20ms: fits 2 Coded S2 K-frames per CI */ + struct ble_gap_upd_params s2_params = { + .itvl_min = 16, + .itvl_max = 16, + .latency = 0, + .supervision_timeout = 2000, + .min_ce_len = 16, + .max_ce_len = 32, + }; + xEventGroupClearBits(coc_event_group, CONN_UPDATED_BIT); + rc = ble_gap_update_params(conn_handle, &s2_params); + if (rc == 0) { + ESP_LOGI(TAG, "Coded S2: updating CI"); + ci_is_slow = true; + xEventGroupWaitBits(coc_event_group, CONN_UPDATED_BIT, pdTRUE, pdTRUE, + pdMS_TO_TICKS(CONN_PARAM_UPDATE_TIMEOUT_MS)); + } else { + ESP_LOGW(TAG, "S2 CI update failed (rc=%d)", rc); + } + } else if (ci_is_slow) { + ci_is_slow = false; + xEventGroupClearBits(coc_event_group, CONN_UPDATED_BIT); + rc = ble_gap_update_params(conn_handle, &conn_params); + if (rc == 0) { + ESP_LOGI(TAG, "%s: restoring CI to 6 ms", phy->name); + xEventGroupWaitBits(coc_event_group, CONN_UPDATED_BIT, pdTRUE, pdTRUE, + pdMS_TO_TICKS(CONN_PARAM_UPDATE_TIMEOUT_MS)); + } else { + ESP_LOGW(TAG, "CI restore failed (rc=%d)", rc); + } + } + + if (chan_stalled) { + ESP_LOGI(TAG, "Pre-drain: waiting for unstall"); + wait_unstall(PREDRAIN_TIMEOUT_MS); + if (chan_stalled) { + ESP_LOGW(TAG, "Pre-drain timed out; forcing clear"); + chan_stalled = false; + } + } + + int64_t start_us = esp_timer_get_time(); + int64_t end_us = start_us + (int64_t)phy->duration_s * 1000000LL; + /* Both the send task and TX_UNSTALLED callback update these counters; a + * lost update is possible on dual-core but harmless for throughput stats. */ + + uint32_t segment_sdus = 0; + uint32_t segment_drops = 0; + + cent_seg_tx_done = &segment_sdus; + cent_seg_tx_drop = &segment_drops; + + xEventGroupClearBits(coc_event_group, TX_UNSTALLED_BIT); /* clear stale signal from previous segment */ + + ESP_LOGI(TAG, "[%s PHY] Sending for %d s", phy->name, phy->duration_s); + + while (!lost_connection && esp_timer_get_time() < end_us) { + /* Snapshot coc_chan — NimBLE host task can NULL it between check and send. */ + struct ble_l2cap_chan *chan = coc_chan; + if (!chan) { + ESP_LOGW(TAG, "COC channel lost during test"); + cent_seg_tx_done = NULL; + cent_seg_tx_drop = NULL; + lost_connection = true; + break; + } + + struct os_mbuf *sdu_tx = os_mbuf_get_pkthdr(&sdu_os_mbuf_pool, 0); + if (!sdu_tx) { + vTaskDelay(1); + continue; + } + + rc = os_mbuf_append(sdu_tx, value, cent_tx_sdu_len); + if (rc != 0) { + os_mbuf_free_chain(sdu_tx); + continue; + } + + rc = ble_l2cap_send(chan, sdu_tx); + + if (rc == 0) { + segment_sdus++; + } else if (rc == BLE_HS_ESTALLED) { + chan_stalled = true; + xEventGroupWaitBits(coc_event_group, TX_UNSTALLED_BIT, pdTRUE, pdTRUE, pdMS_TO_TICKS(100)); + continue; + } else if (rc == BLE_HS_EBUSY) { + os_mbuf_free_chain(sdu_tx); + if (chan_stalled) { + xEventGroupWaitBits(coc_event_group, TX_UNSTALLED_BIT, pdTRUE, pdTRUE, pdMS_TO_TICKS(100)); + } else { + taskYIELD(); + } + continue; + } else if (rc == BLE_HS_ENOMEM) { + vTaskDelay(1); + continue; + } else { + ESP_LOGE(TAG, "Send failed; rc=%d", rc); + if (rc == BLE_HS_EBADDATA) { + os_mbuf_free_chain(sdu_tx); + } + break; + } + + if (segment_sdus % TX_YIELD_INTERVAL == 0 && segment_sdus > 0) { + vTaskDelay(1); + } + } + + if (!coc_chan && !lost_connection) { + cent_seg_tx_done = NULL; + cent_seg_tx_drop = NULL; + lost_connection = true; + } + + if (lost_connection) { + break; + } + + if (chan_stalled) { + ESP_LOGI(TAG, "Post-drain: waiting for unstall"); + wait_unstall(POSTDRAIN_TIMEOUT_MS); + if (chan_stalled) { + ESP_LOGW(TAG, "Post-drain timed out; pre-drain will retry"); + } + } + + int64_t elapsed_us = esp_timer_get_time() - start_us; + if (elapsed_us < 1) { elapsed_us = 1; } + + uint64_t bytes_sent = (uint64_t)segment_sdus * cent_tx_sdu_len; + uint32_t elapsed_ms = (uint32_t)(elapsed_us / 1000); + if (elapsed_ms == 0) { elapsed_ms = 1; } + uint32_t tp_kbps = (uint32_t)((bytes_sent * 8ULL) / elapsed_ms); + uint32_t dropped = segment_drops; + + cent_seg_tx_done = NULL; + cent_seg_tx_drop = NULL; + + ESP_LOGI(TAG, "+-------------------------------------------------+"); + ESP_LOGI(TAG, "| PHY : %-39s|", phy->name); + ESP_LOGI(TAG, "| TX : %-6" PRIu32 " kbps |", tp_kbps); + ESP_LOGI(TAG, "| Bytes : %-10" PRIu64 " |", bytes_sent); + ESP_LOGI(TAG, "| Time : %-5" PRIu32 " s |", elapsed_ms / 1000); + ESP_LOGI(TAG, "+-------------------------------------------------+"); + if (dropped > 0) { + ESP_LOGW(TAG, "%" PRIu32 " SDUs dropped (ENOMEM); raise BT_NIMBLE_MSYS_1_BLOCK_COUNT", + dropped); + } + } + + if (lost_connection) { + ESP_LOGI(TAG, "Waiting for L2CAP COC reconnection..."); + /* disconnect handler already cleared COC_CONNECTED_BIT; clearing it + * again here could cancel a bit set by a reconnect that raced ahead. */ + xEventGroupWaitBits(coc_event_group, COC_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY); + } else { + ESP_LOGI(TAG, "Cycle complete. Looping back to first PHY..."); + } + } +} + +static int cent_gap_event(struct ble_gap_event *event, void *arg) +{ + int rc; + + switch (event->type) { + case BLE_GAP_EVENT_DISC: + cent_connect_if_interesting(&event->disc); + return 0; + +#if CONFIG_EXAMPLE_EXTENDED_ADV + case BLE_GAP_EVENT_EXT_DISC: + cent_connect_if_interesting_ext(&event->ext_disc); + return 0; +#endif + + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + struct ble_gap_conn_desc desc; + if (ble_gap_conn_find(event->connect.conn_handle, &desc) == 0) { + ESP_LOGI(TAG, "Connected; handle=%d peer=%02x:%02x:%02x:%02x:%02x:%02x", + event->connect.conn_handle, + desc.peer_id_addr.val[5], desc.peer_id_addr.val[4], + desc.peer_id_addr.val[3], desc.peer_id_addr.val[2], + desc.peer_id_addr.val[1], desc.peer_id_addr.val[0]); + } + + conn_handle = event->connect.conn_handle; + l2cap_connecting = false; + + rc = ble_hs_hci_util_set_data_len(conn_handle, + LL_PACKET_LENGTH, LL_PACKET_TIME); + if (rc != 0) { + /* DATA_LEN_CHG won't fire — connect L2CAP directly as fallback */ + ESP_LOGE(TAG, "Set packet length failed; rc=%d, connecting L2CAP directly", rc); + l2cap_connecting = true; + cent_l2cap_coc_connect(conn_handle); + } else { + /* DLE accepted; connect L2CAP now — DATA_LEN_CHG may not fire if + * the peer's data length is already at the requested value */ + l2cap_connecting = true; + cent_l2cap_coc_connect(conn_handle); + } + } else { + ESP_LOGE(TAG, "Connection failed; status=%d", event->connect.status); + cent_scan(); + } + return 0; + + case BLE_GAP_EVENT_DISCONNECT: + ESP_LOGI(TAG, "Disconnected; reason=%d", event->disconnect.reason); + conn_handle = BLE_HS_CONN_HANDLE_NONE; + coc_chan = NULL; + chan_stalled = false; + ci_is_slow = false; + l2cap_connecting = false; + xEventGroupClearBits(coc_event_group, COC_CONNECTED_BIT); + xEventGroupSetBits(coc_event_group, TX_UNSTALLED_BIT | CONN_UPDATED_BIT | PHY_UPDATED_BIT); + cent_scan(); + return 0; + + case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE: + ESP_LOGI(TAG, "PHY updated: tx=%d rx=%d status=%d", + event->phy_updated.tx_phy, + event->phy_updated.rx_phy, + event->phy_updated.status); + if (event->phy_updated.status != 0) { + ESP_LOGW(TAG, "PHY update failed; status=%d", event->phy_updated.status); + } + xEventGroupSetBits(coc_event_group, PHY_UPDATED_BIT); + return 0; + + case BLE_GAP_EVENT_CONN_UPDATE: + ESP_LOGI(TAG, "Conn params updated; status=%d", event->conn_update.status); + if (event->conn_update.status != 0) { + ESP_LOGW(TAG, "Connection parameter update failed (status=%d)", + event->conn_update.status); + } + xEventGroupSetBits(coc_event_group, CONN_UPDATED_BIT); + return 0; + + case BLE_GAP_EVENT_DATA_LEN_CHG: + /* fires for TX and RX; guard ensures connect called only once */ + if (!l2cap_connecting && coc_chan == NULL) { + l2cap_connecting = true; + cent_l2cap_coc_connect(conn_handle); + } + return 0; + + case BLE_GAP_EVENT_DISC_COMPLETE: + ESP_LOGI(TAG, "Discovery complete; reason=%d", event->disc_complete.reason); + return 0; + + default: + return 0; + } +} + +static void cent_on_reset(int reason) +{ + ESP_LOGE(TAG, "Host reset; reason=%d", reason); +} + +static void cent_on_sync(void) +{ + int rc = ble_hs_util_ensure_addr(0); + assert(rc == 0); + + uint8_t own_addr_type; + uint8_t addr[6] = {0}; + + rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) { + ESP_LOGE(TAG, "Error inferring addr type; rc=%d", rc); + return; + } + ble_hs_id_copy_addr(own_addr_type, addr, NULL); + ESP_LOGI(TAG, "Device Address: %02x:%02x:%02x:%02x:%02x:%02x", + addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]); + + cent_scan(); +} + +static void cent_host_task(void *param) +{ + ESP_LOGI(TAG, "BLE Host Task started"); + nimble_port_run(); + nimble_port_freertos_deinit(); +} + +void app_main(void) +{ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + coc_event_group = xEventGroupCreate(); + assert(coc_event_group); + + ret = nimble_port_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "nimble_port_init failed; rc=%d", ret); + return; + } + + cent_l2cap_coc_mem_init(); + + ble_hs_cfg.reset_cb = cent_on_reset; + ble_hs_cfg.sync_cb = cent_on_sync; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + +#if CONFIG_BT_NIMBLE_GAP_SERVICE + int rc = ble_svc_gap_device_name_set("l2cap-coc-cent"); + assert(rc == 0); +#endif + + ble_store_config_init(); + + if (xTaskCreate(cent_send_task, "cent_send_task", 4096, NULL, 5, NULL) != pdPASS) { + ESP_LOGE(TAG, "Failed to create cent_send_task"); + return; + } + + nimble_port_freertos_init(cent_host_task); +} diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults new file mode 100644 index 00000000000..484df7d544b --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults @@ -0,0 +1,16 @@ +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=512 +CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=255 +CONFIG_BT_NIMBLE_LOG_LEVEL=4 +CONFIG_BT_NIMBLE_LOG_LEVEL_NONE=y +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=400 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=255 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=50 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_SIZE=260 +CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM=1 +CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT=12 +CONFIG_EXAMPLE_L2CAP_COC_MTU=2048 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67 +CONFIG_FREERTOS_HZ=1000 +CONFIG_ESP_TASK_WDT_TIMEOUT_S=30 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32 b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32 new file mode 100644 index 00000000000..d11249b3f31 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32 @@ -0,0 +1,4 @@ +CONFIG_EXAMPLE_L2CAP_COC_MTU=2048 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=96 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=48 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=10 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32c2 b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32c2 new file mode 100644 index 00000000000..7afcaef3d97 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32c2 @@ -0,0 +1,6 @@ +# ACL_FROM_LL_COUNT kept at 67 (not reduced like MSYS pools) to avoid 0 kbps +# throughput after Coded S2 <-> S8 PHY switches on this RAM-limited target. +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=12 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=292 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=0 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32c3 b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32c3 new file mode 100644 index 00000000000..8fd238fb854 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32c3 @@ -0,0 +1,4 @@ +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=160 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=48 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24 +CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=8 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32c6 b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32c6 new file mode 100644 index 00000000000..589bf21d601 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32c6 @@ -0,0 +1,3 @@ +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=120 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=48 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32h2 b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32h2 new file mode 100644 index 00000000000..589bf21d601 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/sdkconfig.defaults.esp32h2 @@ -0,0 +1,3 @@ +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=120 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=48 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/CMakeLists.txt b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/CMakeLists.txt new file mode 100644 index 00000000000..c4181070e58 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(l2cap_coc_prph) diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/README.md b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/README.md new file mode 100644 index 00000000000..e1324b34c0c --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/README.md @@ -0,0 +1,77 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-S3 | ESP32-S31 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | --------- | + +# L2CAP COC Throughput Peripheral Example + +`l2cap_coc_prph` demonstrates the peripheral side of an L2CAP Connection-Oriented Channel (COC) throughput test using NimBLE on ESP32. It advertises with UUID 0x1812, accepts an incoming GAP connection from `l2cap_coc_cent`, registers an L2CAP COC server on PSM 0x1002, and measures RX throughput as the central sends SDUs. + +The peripheral tracks throughput per PHY — each time the central switches PHY, the peripheral prints a per-PHY throughput summary box and resets its counters. A background stats task also prints a live per-second RX rate while data is flowing. It must be used together with the `l2cap_coc_cent` example which acts as the sending side. + +It uses ESP32's Bluetooth controller and NimBLE stack based BLE host. + +## How to Use Example + +Before project configuration and build, be sure to set the correct chip target using: + +```bash +idf.py set-target +``` + +### Hardware Required + +* Two development boards, one flashed with `l2cap_coc_prph` and the other with `l2cap_coc_cent`. +* A USB cable for power supply and programming. + +See [Development Boards](https://www.espressif.com/en/products/devkits) for more information. + +### Configure the Project + +Open the project configuration menu: + +```bash +idf.py menuconfig +``` + +In the `L2CAP COC Throughput Configuration` menu: + +| Option | Default | Description | +|---------|---------|-------------| +| `EXAMPLE_L2CAP_COC_MTU` | `16384` | Peripheral L2CAP CoC SDU MTU size in bytes. | +| `EXAMPLE_EXTENDED_ADV` | `y` (BLE 5.0 chips) | Enable extended advertising for BLE 5.0 capable devices. Required for Coded PHY testing on ESP32-C6 and ESP32-H2. | + +> **Note:** Throughput in the central → peripheral direction is primarily determined by the peripheral MTU. With the default configuration (`MTU=16384`, `MPS=247`), NimBLE grants approximately 67 initial credits to the central sender, allowing multiple packets to remain in flight and maximizing link throughput. + +### Build and Flash + +Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +On successful connection and data reception, the peripheral prints a live per-second RX rate while data flows: + +``` +I (xxx) l2cap_coc_prph: BLE Host Task started +I (xxx) l2cap_coc_prph: Device Address: xx:xx:xx:xx:xx:xx +I (xxx) l2cap_coc_prph: Extended advertising started +I (xxx) l2cap_coc_prph: Connected; handle=0 +I (xxx) l2cap_coc_prph: L2CAP COC connected, chan=0xxxxxxxxx +I (xxx) l2cap_coc_prph: PHY updated: tx=2 rx=2 status=0 +I (xxx) l2cap_coc_prph: | RX : xxxx kbps | +I (xxx) l2cap_coc_prph: | RX : xxxx kbps | +I (xxx) l2cap_coc_prph: | RX : xxxx kbps | +I (xxx) l2cap_coc_prph: | RX : xxxx kbps | +I (xxx) l2cap_coc_prph: | RX : xxxx kbps | +I (xxx) l2cap_coc_prph: | RX : xxxx kbps | +I (xxx) l2cap_coc_prph: | RX : xxxx kbps | +I (xxx) l2cap_coc_prph: | RX : xxxx kbps | +``` + +> **Note:** The peripheral prints one RX line per second. The central controls PHY selection and test duration; the peripheral tracks and displays throughput continuously as long as data is flowing. + +## Troubleshooting + +For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/main/CMakeLists.txt b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/main/CMakeLists.txt new file mode 100644 index 00000000000..718f89cab8e --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "main.c" + PRIV_REQUIRES bt nvs_flash esp_timer + INCLUDE_DIRS ".") diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/main/Kconfig.projbuild b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/main/Kconfig.projbuild new file mode 100644 index 00000000000..ee49e5894fd --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/main/Kconfig.projbuild @@ -0,0 +1,23 @@ +menu "L2CAP COC Throughput Configuration" + + config EXAMPLE_L2CAP_COC_MTU + int "L2CAP CoC MTU size in bytes" + default 16384 + range 512 65511 + help + L2CAP CoC SDU MTU size in bytes. + Note: memory pool allocates 6 buffers of this size; total pool + memory = MTU x 6. On chips without PSRAM ensure sufficient heap + is available before increasing this value. + Use idf.py size-components to verify. + + config EXAMPLE_EXTENDED_ADV + bool + depends on SOC_BLE_50_SUPPORTED && BT_NIMBLE_50_FEATURE_SUPPORT + default y if SOC_ESP_NIMBLE_CONTROLLER + select BT_NIMBLE_EXT_ADV + prompt "Enable Extended Advertising" + help + Enable BLE 5.0 extended advertising. + +endmenu diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/main/main.c b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/main/main.c new file mode 100644 index 00000000000..99767c15b84 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/main/main.c @@ -0,0 +1,445 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "host/ble_hs.h" +#include "host/util/util.h" +#include "services/gap/ble_svc_gap.h" + +static const char *TAG = "l2cap_coc_prph"; + +#define L2CAP_COC_PSM 0x1002 +#define L2CAP_COC_MTU CONFIG_EXAMPLE_L2CAP_COC_MTU +#define COC_BUF_COUNT (6 * MYNEWT_VAL(BLE_L2CAP_COC_MAX_NUM)) +/* Block size must include mbuf headers so each SDU fits in one pool entry. */ +#define SDU_BLOCK_SIZE (L2CAP_COC_MTU + sizeof(struct os_mbuf_pkthdr) + sizeof(struct os_mbuf)) +#define LL_PACKET_LENGTH 251 +#define LL_PACKET_TIME 2120 +#define L2CAP_COC_UUID 0x1812 + +static uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE; +static struct ble_l2cap_chan *coc_chan = NULL; +static uint8_t own_addr_type; + +static int64_t phy_start_time = 0; +static volatile uint32_t rx_bytes = 0; +static uint32_t rx_packets = 0; +static volatile bool coc_active = false; +static const char *phy_name = "1M"; +static uint8_t current_phy = BLE_HCI_LE_PHY_1M; + +void ble_store_config_init(void); + +static os_membuf_t sdu_coc_mem[OS_MEMPOOL_SIZE(COC_BUF_COUNT, SDU_BLOCK_SIZE)]; +static struct os_mempool sdu_coc_mempool; +static struct os_mbuf_pool sdu_os_mbuf_pool; + +static int prph_gap_event(struct ble_gap_event *event, void *arg); + +static const char *prph_phy_str(uint8_t phy) +{ + switch (phy) { + case BLE_HCI_LE_PHY_2M: return "2M"; + case BLE_HCI_LE_PHY_CODED: return "Coded"; + default: return "1M"; + } +} + +static void prph_report_phy(int64_t end_time, int64_t start_time, + uint32_t bytes, uint32_t packets, + const char *phy_name) +{ + if (packets == 0 || start_time == 0) { + return; + } + int64_t elapsed_ms = (end_time - start_time) / 1000; + if (elapsed_ms == 0) { elapsed_ms = 1; } + uint32_t kbps = (uint32_t)((uint64_t)bytes * 8ULL + / (uint64_t)elapsed_ms); + ESP_LOGI(TAG, "+-------------------------------------------------+"); + ESP_LOGI(TAG, "| PHY : %-39s|", phy_name); + ESP_LOGI(TAG, "| RX : %-6" PRIu32 " kbps |", kbps); + ESP_LOGI(TAG, "| Bytes : %-10" PRIu32 " |", bytes); + ESP_LOGI(TAG, "| Time : %-5lld s |", elapsed_ms / 1000); + ESP_LOGI(TAG, "+-------------------------------------------------+"); +} + +#if CONFIG_EXAMPLE_EXTENDED_ADV +static uint8_t ext_adv_pattern[] = { + 0x02, BLE_HS_ADV_TYPE_FLAGS, 0x06, + 0x03, BLE_HS_ADV_TYPE_COMP_UUIDS16, 0x12, 0x18, + 0x11, BLE_HS_ADV_TYPE_COMP_NAME, + 'l','2','c','a','p','-','c','o','c','-','p','r','p','h','-','e', +}; + +static void prph_advertise(void) +{ + struct ble_gap_ext_adv_params params; + struct os_mbuf *data; + uint8_t instance = 0; + int rc; + + memset(¶ms, 0, sizeof(params)); + params.connectable = 1; + params.own_addr_type = own_addr_type; + params.primary_phy = BLE_HCI_LE_PHY_1M; + params.secondary_phy = BLE_HCI_LE_PHY_1M; + params.tx_power = 127; + params.sid = 1; + params.itvl_min = BLE_GAP_ADV_FAST_INTERVAL1_MIN; + params.itvl_max = BLE_GAP_ADV_FAST_INTERVAL1_MIN; + + rc = ble_gap_ext_adv_configure(instance, ¶ms, NULL, prph_gap_event, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "ext_adv_configure failed; rc=%d", rc); + return; + } + + data = os_msys_get_pkthdr(sizeof(ext_adv_pattern), 0); + if (!data) { + ESP_LOGE(TAG, "ext_adv: failed to alloc adv data mbuf"); + return; + } + rc = os_mbuf_append(data, ext_adv_pattern, sizeof(ext_adv_pattern)); + if (rc != 0) { + ESP_LOGE(TAG, "ext_adv: mbuf_append failed; rc=%d", rc); + os_mbuf_free_chain(data); + return; + } + + rc = ble_gap_ext_adv_set_data(instance, data); + if (rc != 0) { + ESP_LOGE(TAG, "ext_adv_set_data failed; rc=%d", rc); + return; + } + + rc = ble_gap_ext_adv_start(instance, 0, 0); + if (rc != 0) { + ESP_LOGE(TAG, "ext_adv_start failed; rc=%d", rc); + return; + } + ESP_LOGI(TAG, "Extended advertising started"); +} +#else +static void prph_advertise(void) +{ + struct ble_gap_adv_params adv_params; + struct ble_hs_adv_fields fields; + int rc; + + memset(&fields, 0, sizeof(fields)); + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + fields.tx_pwr_lvl_is_present = 1; + fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; +#if CONFIG_BT_NIMBLE_GAP_SERVICE + const char *name = ble_svc_gap_device_name(); + fields.name = (uint8_t *)name; + fields.name_len = strlen(name); + fields.name_is_complete = 1; +#endif + fields.uuids16 = (ble_uuid16_t[]){ BLE_UUID16_INIT(L2CAP_COC_UUID) }; + fields.num_uuids16 = 1; + fields.uuids16_is_complete = 1; + + rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + ESP_LOGE(TAG, "Error setting adv data; rc=%d", rc); + return; + } + + memset(&adv_params, 0, sizeof(adv_params)); + adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; + adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; + rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, + &adv_params, prph_gap_event, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Error starting adv; rc=%d", rc); + } +} +#endif /* CONFIG_EXAMPLE_EXTENDED_ADV */ + +static void prph_l2cap_coc_mem_init(void) +{ + int rc; + rc = os_mempool_init(&sdu_coc_mempool, COC_BUF_COUNT, SDU_BLOCK_SIZE, + sdu_coc_mem, "prph_coc_pool"); + assert(rc == 0); + rc = os_mbuf_pool_init(&sdu_os_mbuf_pool, &sdu_coc_mempool, SDU_BLOCK_SIZE, + COC_BUF_COUNT); + assert(rc == 0); +} + +static int prph_l2cap_coc_accept(struct ble_l2cap_chan *chan) +{ + struct os_mbuf *sdu_rx = os_mbuf_get_pkthdr(&sdu_os_mbuf_pool, 0); + if (!sdu_rx) { + return BLE_HS_ENOMEM; + } + int rc = ble_l2cap_recv_ready(chan, sdu_rx); + if (rc != 0) { + os_mbuf_free_chain(sdu_rx); + } + return rc; +} + +static int prph_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg) +{ + switch (event->type) { + case BLE_L2CAP_EVENT_COC_CONNECTED: + if (event->connect.status != 0) { + ESP_LOGE(TAG, "L2CAP COC connect error: %d", event->connect.status); + return 0; + } + ESP_LOGI(TAG, "L2CAP COC connected, chan=%p", event->connect.chan); + coc_chan = event->connect.chan; + phy_start_time = 0; /* anchored on first data SDU, not connect */ + rx_bytes = 0; + rx_packets = 0; + coc_active = true; + phy_name = prph_phy_str(current_phy); + return 0; + + case BLE_L2CAP_EVENT_COC_DISCONNECTED: + coc_active = false; + coc_chan = NULL; + current_phy = BLE_HCI_LE_PHY_1M; + { + int64_t end = esp_timer_get_time(); + int64_t st = phy_start_time; + uint32_t by = rx_bytes; + uint32_t pk = rx_packets; + prph_report_phy(end, st, by, pk, phy_name); + rx_bytes = 0; rx_packets = 0; phy_start_time = 0; + } + ESP_LOGI(TAG, "L2CAP COC disconnected"); + return 0; + + case BLE_L2CAP_EVENT_COC_ACCEPT: { + /* Pre-grant 2 receive buffers so the central can pipeline 2 SDUs. */ + int rc = prph_l2cap_coc_accept(event->accept.chan); + if (rc != 0) { + return rc; + } + /* Second buffer is best-effort; one buffer is enough for the channel to operate. */ + if (prph_l2cap_coc_accept(event->accept.chan) != 0) { + ESP_LOGW(TAG, "L2CAP COC accept: second RX buffer unavailable, running with one"); + } + return 0; + } + + case BLE_L2CAP_EVENT_COC_DATA_RECEIVED: + if (event->receive.sdu_rx) { + if (rx_packets == 0) { + phy_start_time = esp_timer_get_time(); + } + rx_bytes += OS_MBUF_PKTLEN(event->receive.sdu_rx); + rx_packets += 1; + os_mbuf_free_chain(event->receive.sdu_rx); + } + if (prph_l2cap_coc_accept(event->receive.chan) != 0) { + ESP_LOGE(TAG, "DATA_RECEIVED: no mbuf for recv_ready; RX may stall"); + } + return 0; + + default: + return 0; + } +} + +static void prph_stats_task(void *arg) +{ + uint32_t prev_bytes = 0; + int64_t prev_time = 0; + + while (1) { + vTaskDelay(pdMS_TO_TICKS(1000)); + + if (!coc_active) { + prev_bytes = 0; + prev_time = 0; + continue; + } + + int64_t now = esp_timer_get_time(); + uint32_t bytes = rx_bytes; + + if (prev_time > 0) { + if (bytes < prev_bytes) { + prev_bytes = bytes; + prev_time = now; + continue; + } + int64_t dt_us = now - prev_time; + uint32_t dt_bytes = bytes - prev_bytes; + uint32_t kbps = (uint32_t)((uint64_t)dt_bytes * 8ULL * 1000000ULL + / (uint64_t)dt_us / 1000ULL); + ESP_LOGI(TAG, "| RX : %-6" PRIu32 " kbps |", kbps); + } + + prev_bytes = bytes; + prev_time = now; + } +} + +static int prph_gap_event(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status != 0) { + ESP_LOGE(TAG, "Connection failed; status=%d", event->connect.status); + prph_advertise(); + return 0; + } + ESP_LOGI(TAG, "Connected; handle=%d", event->connect.conn_handle); + conn_handle = event->connect.conn_handle; + return 0; + + case BLE_GAP_EVENT_DISCONNECT: + ESP_LOGI(TAG, "Disconnected; reason=%d", event->disconnect.reason); + conn_handle = BLE_HS_CONN_HANDLE_NONE; + coc_chan = NULL; + coc_active = false; + current_phy = BLE_HCI_LE_PHY_1M; + phy_name = "1M"; +#if CONFIG_EXAMPLE_EXTENDED_ADV + ble_gap_ext_adv_stop(0); +#endif + prph_advertise(); + return 0; + + case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE: + ESP_LOGI(TAG, "PHY updated: tx=%d rx=%d status=%d", + event->phy_updated.tx_phy, + event->phy_updated.rx_phy, + event->phy_updated.status); + if (event->phy_updated.status == 0) { + if (coc_active) { + int64_t end = esp_timer_get_time(); + int64_t st = phy_start_time; + uint32_t by = rx_bytes; + uint32_t pk = rx_packets; + prph_report_phy(end, st, by, pk, phy_name); + rx_bytes = 0; rx_packets = 0; phy_start_time = 0; + } + current_phy = event->phy_updated.rx_phy; + phy_name = prph_phy_str(event->phy_updated.rx_phy); + } + return 0; + + case BLE_GAP_EVENT_CONN_UPDATE: + ESP_LOGI(TAG, "Conn params updated; status=%d", event->conn_update.status); + if (event->conn_update.status == 0 && coc_active && + current_phy == BLE_HCI_LE_PHY_CODED) { + struct ble_gap_conn_desc desc; + if (ble_gap_conn_find(conn_handle, &desc) == 0) { + if (desc.conn_itvl != 16 && desc.conn_itvl != 32) { + return 0; + } + if (strcmp(phy_name, "Coded") != 0) { + int64_t end = esp_timer_get_time(); + uint32_t by = rx_bytes; + uint32_t pk = rx_packets; + prph_report_phy(end, phy_start_time, by, pk, phy_name); + rx_bytes = 0; rx_packets = 0; phy_start_time = 0; + } + phy_name = (desc.conn_itvl >= 32) ? "Coded S8" : "Coded S2"; + ESP_LOGI(TAG, "Coding scheme updated to %s (CI=%u × 1.25ms)", + phy_name, desc.conn_itvl); + } + } + return 0; + + case BLE_GAP_EVENT_ADV_COMPLETE: +#if !CONFIG_EXAMPLE_EXTENDED_ADV + prph_advertise(); +#endif + return 0; + + default: + return 0; + } +} + +static void prph_on_reset(int reason) +{ + ESP_LOGE(TAG, "Host reset; reason=%d", reason); +} + +static void prph_on_sync(void) +{ + int rc; + + rc = ble_hs_util_ensure_addr(0); + assert(rc == 0); + + rc = ble_hs_id_infer_auto(0, &own_addr_type); + assert(rc == 0); + + rc = ble_l2cap_create_server(L2CAP_COC_PSM, L2CAP_COC_MTU, + prph_l2cap_coc_event_cb, NULL); + if (rc != 0 && rc != BLE_HS_EALREADY) { + ESP_LOGE(TAG, "Failed to create L2CAP COC server; rc=%d", rc); + return; + } + + uint8_t addr[6] = {0}; + ble_hs_id_copy_addr(own_addr_type, addr, NULL); + ESP_LOGI(TAG, "Device Address: %02x:%02x:%02x:%02x:%02x:%02x", + addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]); + + prph_advertise(); +} + +static void prph_host_task(void *param) +{ + ESP_LOGI(TAG, "BLE Host Task started"); + nimble_port_run(); + nimble_port_freertos_deinit(); +} + +void app_main(void) +{ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ret = nimble_port_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "nimble_port_init failed; rc=%d", ret); + return; + } + + prph_l2cap_coc_mem_init(); + + ble_hs_cfg.reset_cb = prph_on_reset; + ble_hs_cfg.sync_cb = prph_on_sync; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + +#if CONFIG_BT_NIMBLE_GAP_SERVICE + int rc = ble_svc_gap_device_name_set("l2cap-coc-prph"); + assert(rc == 0); +#endif + + ble_store_config_init(); + + if (xTaskCreate(prph_stats_task, "prph_stats", 4096, NULL, 5, NULL) != pdPASS) { + ESP_LOGE(TAG, "Failed to create stats task"); + } + + nimble_port_freertos_init(prph_host_task); +} diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults new file mode 100644 index 00000000000..441f5c6c3a2 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults @@ -0,0 +1,15 @@ +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=512 +CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=255 +CONFIG_BT_NIMBLE_LOG_LEVEL=4 +CONFIG_BT_NIMBLE_LOG_LEVEL_NONE=y +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=400 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=255 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=50 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_SIZE=260 +CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM=1 +CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT=12 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24 +CONFIG_FREERTOS_HZ=1000 +CONFIG_ESP_TASK_WDT_TIMEOUT_S=30 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32 b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32 new file mode 100644 index 00000000000..796c5e2681c --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32 @@ -0,0 +1,4 @@ +CONFIG_EXAMPLE_L2CAP_COC_MTU=8192 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=20 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=20 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=10 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32c2 b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32c2 new file mode 100644 index 00000000000..3d6eecb81ac --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32c2 @@ -0,0 +1,7 @@ +# MTU is intentionally left at the Kconfig default (16384) for throughput. +# Reducing to 2048 cuts credits from ~67 to ~9, dropping throughput ~8x. +# If the build fails due to RAM pressure (~96 KB static SDU pool), lower MTU here. +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=12 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=292 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=0 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32c3 b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32c3 new file mode 100644 index 00000000000..77211f1473e --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32c3 @@ -0,0 +1,6 @@ +# MTU is intentionally left at the Kconfig default (16384) for throughput. +# Reducing MTU lowers L2CAP credit flow, which directly cuts throughput. +# If the build fails due to RAM pressure (~98 KB static SDU pool), lower MTU here. +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=20 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24 +CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=8 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32c6 b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32c6 new file mode 100644 index 00000000000..a9588a5cd15 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32c6 @@ -0,0 +1,2 @@ +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=30 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67 diff --git a/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32h2 b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32h2 new file mode 100644 index 00000000000..a9588a5cd15 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/sdkconfig.defaults.esp32h2 @@ -0,0 +1,2 @@ +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=30 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67 diff --git a/tools/ci/check_examples_documented.py b/tools/ci/check_examples_documented.py index 5faeceb0e65..073676a8539 100644 --- a/tools/ci/check_examples_documented.py +++ b/tools/ci/check_examples_documented.py @@ -103,8 +103,10 @@ KNOWN_MISSING = { 'bluetooth/nimble/bleprph_host_only', 'bluetooth/nimble/bleprph_wifi_coex', 'bluetooth/nimble/hci', - 'bluetooth/nimble/throughput_app/blecent_throughput', - 'bluetooth/nimble/throughput_app/bleprph_throughput', + 'bluetooth/nimble/throughput_app/gatt/blecent_throughput', + 'bluetooth/nimble/throughput_app/gatt/bleprph_throughput', + 'bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent', + 'bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph', # TODO IDF-15385: add :example: references for build_system examples 'build_system/cmake/import_prebuilt/prebuilt', 'build_system/cmakev2/features/component_manager',