Merge branch 'bugfix/nimble_issues_26082026_v6.1' into 'release/v6.1'

fix(nimble): Fix few nimble issues 26082026 (v6.1)

See merge request espressif/esp-idf!52075
This commit is contained in:
Rahul Tank
2026-08-29 21:17:04 +05:30
84 changed files with 5932 additions and 196 deletions
+5
View File
@@ -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
+5
View File
@@ -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"
+22
View File
@@ -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)
+7
View File
@@ -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"
@@ -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.
@@ -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
@@ -0,0 +1,109 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#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;
}
+2
View File
@@ -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
+30
View File
@@ -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 || \
+686
View File
@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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_ */
@@ -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_ */
@@ -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 <assert.h>
#include <stdint.h>
#include <string.h>
#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_ */
File diff suppressed because it is too large Load Diff
-17
View File
@@ -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))
@@ -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;
}
+36 -4
View File
@@ -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"
@@ -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
@@ -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
@@ -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
@@ -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
+1 -1
View File
@@ -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
}
@@ -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;
@@ -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
@@ -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)
@@ -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 <stdbool.h>
#include <stdint.h>
#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(&not_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(&not_inited, 0));
TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_mutex_release(&not_inited));
TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_mutex_deinit(&not_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(&not_inited, 0));
TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_sem_release(&not_inited));
TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_sem_deinit(&not_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();
}
@@ -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 <stdbool.h>
#include <stdint.h>
#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();
}
@@ -1,2 +1,3 @@
CONFIG_BT_ENABLED=y
CONFIG_BT_PRF_TASK_ENABLED=y
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
@@ -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);
}
@@ -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;
+18 -6
View File
@@ -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/**/*
@@ -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(&params);
@@ -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;
@@ -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();
@@ -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 {
@@ -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 |
@@ -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
@@ -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
@@ -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)
@@ -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 <chip_name>
```
### 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.
@@ -0,0 +1,3 @@
idf_component_register(SRCS "main.c"
PRIV_REQUIRES bt nvs_flash esp_timer
INCLUDE_DIRS ".")
@@ -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
@@ -0,0 +1,3 @@
dependencies:
nimble_central_utils:
path: ${IDF_PATH}/examples/bluetooth/nimble/common/nimble_central_utils
@@ -0,0 +1,738 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#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);
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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 <chip_name>
```
### 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.
@@ -0,0 +1,3 @@
idf_component_register(SRCS "main.c"
PRIV_REQUIRES bt nvs_flash esp_timer
INCLUDE_DIRS ".")
@@ -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
@@ -0,0 +1,445 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#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(&params, 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, &params, 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);
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=30
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67
@@ -0,0 +1,2 @@
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=30
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67
+4 -2
View File
@@ -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',