mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 03:00:34 +03:00
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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 || \
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Submodule components/bt/host/nimble/nimble updated: 139cada0ae...96ab0e4d86
@@ -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
|
||||
|
||||
@@ -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(¬_inited, &s_test_task_info));
|
||||
|
||||
bt_osal_eventq_init(&evq);
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK,
|
||||
bt_osal_eventq_start(&evq, &s_test_task_info));
|
||||
/* Only one task may service a queue. */
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL,
|
||||
bt_osal_eventq_start(&evq, &s_test_task_info));
|
||||
|
||||
bt_osal_event_init(&ev, ev_cb_count, (void *)0xC0FFEE);
|
||||
bt_osal_eventq_put(&evq, &ev);
|
||||
|
||||
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS)));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, s_run_count);
|
||||
TEST_ASSERT_EQUAL_PTR(&ev, s_last_ev);
|
||||
TEST_ASSERT_EQUAL_PTR((void *)0xC0FFEE, s_last_arg);
|
||||
/* The worker cleared the queued flag before running the handler. */
|
||||
TEST_ASSERT_FALSE(bt_osal_event_is_queued(&ev));
|
||||
|
||||
bt_osal_event_deinit(&ev);
|
||||
/* deinit() stops the worker task before releasing the queue it waits on. */
|
||||
bt_osal_eventq_deinit(&evq);
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
|
||||
TEST_CASE("osal eventq worker honours the requested core affinity", "[osal]")
|
||||
{
|
||||
struct bt_osal_eventq evq = {0};
|
||||
struct bt_osal_event ev = {0};
|
||||
struct bt_osal_task_info info = s_test_task_info;
|
||||
|
||||
osal_test_begin();
|
||||
|
||||
bt_osal_eventq_init(&evq);
|
||||
|
||||
/* A core that does not exist is rejected instead of asserting inside FreeRTOS. */
|
||||
info.core_id = portNUM_PROCESSORS;
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, bt_osal_eventq_start(&evq, &info));
|
||||
info.core_id = -2;
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, bt_osal_eventq_start(&evq, &info));
|
||||
|
||||
/* Pinned to CPU0, the worker must dispatch from CPU0. */
|
||||
info.core_id = 0;
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_eventq_start(&evq, &info));
|
||||
|
||||
s_last_core = -1;
|
||||
bt_osal_event_init(&ev, ev_cb_core, NULL);
|
||||
bt_osal_eventq_put(&evq, &ev);
|
||||
|
||||
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS)));
|
||||
TEST_ASSERT_EQUAL_INT(0, s_last_core);
|
||||
|
||||
bt_osal_event_deinit(&ev);
|
||||
bt_osal_eventq_deinit(&evq);
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
|
||||
TEST_CASE("osal eventq_post_func runs a one-shot handler", "[osal]")
|
||||
{
|
||||
struct bt_osal_eventq evq = {0};
|
||||
|
||||
osal_test_begin();
|
||||
|
||||
bt_osal_eventq_init(&evq);
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK,
|
||||
bt_osal_eventq_start(&evq, &s_test_task_info));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, bt_osal_eventq_post_func(&evq, NULL, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, bt_osal_eventq_post_func(NULL, ev_cb_count, NULL));
|
||||
|
||||
/* The event is allocated by the layer and freed after the handler returns;
|
||||
* a leak would be caught by the heap check in tearDown().
|
||||
*/
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK,
|
||||
bt_osal_eventq_post_func(&evq, ev_cb_count, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK,
|
||||
bt_osal_eventq_post_func(&evq, ev_cb_count, (void *)0xABCD));
|
||||
|
||||
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS)));
|
||||
TEST_ASSERT_EQUAL_UINT32(2, s_run_count);
|
||||
TEST_ASSERT_EQUAL_PTR((void *)0xABCD, s_last_arg);
|
||||
|
||||
bt_osal_eventq_deinit(&evq);
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
|
||||
TEST_CASE("osal eventq_deinit reclaims still-queued post_func items", "[osal]")
|
||||
{
|
||||
struct bt_osal_eventq evq = {0};
|
||||
|
||||
osal_test_begin();
|
||||
|
||||
bt_osal_eventq_init(&evq);
|
||||
|
||||
/* Post one-shot items but never start a worker, so they stay queued and are
|
||||
* never dispatched. Their backing storage is owned by the layer, so
|
||||
* eventq_deinit() must reclaim it; otherwise the heap check in tearDown()
|
||||
* would report a leak.
|
||||
*/
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_eventq_post_func(&evq, ev_cb_count, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_eventq_post_func(&evq, ev_cb_count, (void *)0x1));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_eventq_post_func(&evq, ev_cb_count, (void *)0x2));
|
||||
TEST_ASSERT_FALSE(bt_osal_eventq_is_empty(&evq));
|
||||
|
||||
bt_osal_eventq_deinit(&evq);
|
||||
TEST_ASSERT_NULL(evq.eventq);
|
||||
/* The items were reclaimed on teardown, not dispatched. */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, s_run_count);
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
|
||||
/* Producer for the concurrency test: posts every event of the array to the queue. */
|
||||
static void eventq_producer_task(void *arg)
|
||||
{
|
||||
struct bt_osal_eventq *evq = (struct bt_osal_eventq *)arg;
|
||||
static struct bt_osal_event evs[8];
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
evs[i] = (struct bt_osal_event){0};
|
||||
bt_osal_event_init(&evs[i], ev_cb_count, (void *)(uintptr_t)i);
|
||||
bt_osal_eventq_put(evq, &evs[i]);
|
||||
}
|
||||
|
||||
/* Hand the events back to the test task so it can release them. */
|
||||
s_last_arg = evs;
|
||||
xSemaphoreGive(s_done);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
TEST_CASE("osal eventq delivers every event under a concurrent producer", "[osal]")
|
||||
{
|
||||
struct bt_osal_eventq evq = {0};
|
||||
struct bt_osal_event *evs;
|
||||
bool seen[8] = {false};
|
||||
|
||||
osal_test_begin();
|
||||
|
||||
bt_osal_eventq_init(&evq);
|
||||
|
||||
TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(eventq_producer_task, "osal_prod", TEST_HELPER_STACK,
|
||||
&evq, uxTaskPriorityGet(NULL), NULL));
|
||||
|
||||
/* Drain the queue: each event must come out exactly once. */
|
||||
for (int i = 0; i < 8; i++) {
|
||||
struct bt_osal_event *ev = bt_osal_eventq_get(&evq, pdMS_TO_TICKS(TEST_WAIT_MS));
|
||||
TEST_ASSERT_NOT_NULL(ev);
|
||||
|
||||
uintptr_t idx = (uintptr_t)bt_osal_event_get_arg(ev);
|
||||
TEST_ASSERT_LESS_THAN_UINT32(8, idx);
|
||||
TEST_ASSERT_FALSE(seen[idx]);
|
||||
seen[idx] = true;
|
||||
}
|
||||
TEST_ASSERT_TRUE(bt_osal_eventq_is_empty(&evq));
|
||||
|
||||
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS)));
|
||||
evs = (struct bt_osal_event *)s_last_arg;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
bt_osal_event_deinit(&evs[i]);
|
||||
}
|
||||
|
||||
bt_osal_eventq_deinit(&evq);
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
|
||||
/* Holds the mutex until the test task tells it to let go. */
|
||||
static SemaphoreHandle_t s_holder_ready;
|
||||
|
||||
static void mutex_holder_task(void *arg)
|
||||
{
|
||||
struct bt_osal_mutex *mu = (struct bt_osal_mutex *)arg;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_pend(mu, BT_OSAL_TIME_FOREVER));
|
||||
xSemaphoreGive(s_holder_ready);
|
||||
|
||||
/* Wait for the test task to finish its contended pend, then hand the mutex back. */
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_release(mu));
|
||||
|
||||
xSemaphoreGive(s_done);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
TEST_CASE("osal mutex is recursive and pend times out while held", "[osal]")
|
||||
{
|
||||
struct bt_osal_mutex mu = {0};
|
||||
struct bt_osal_mutex not_inited = {0};
|
||||
|
||||
osal_test_begin();
|
||||
|
||||
/* Operations on a mutex that was never initialized are rejected. */
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_mutex_pend(¬_inited, 0));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_mutex_release(¬_inited));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_mutex_deinit(¬_inited));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_init(&mu));
|
||||
TEST_ASSERT_NOT_NULL(mu.mutex);
|
||||
|
||||
/* The owner may take it again without deadlocking. */
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_pend(&mu, 0));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_pend(&mu, 0));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_release(&mu));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_release(&mu));
|
||||
|
||||
s_holder_ready = xSemaphoreCreateBinary();
|
||||
TEST_ASSERT_NOT_NULL(s_holder_ready);
|
||||
|
||||
TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(mutex_holder_task, "osal_hold", TEST_HELPER_STACK,
|
||||
&mu, uxTaskPriorityGet(NULL), NULL));
|
||||
|
||||
/* Another task owns the mutex now: a bounded pend must give up. */
|
||||
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_holder_ready, pdMS_TO_TICKS(TEST_WAIT_MS)));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_TIMEOUT, bt_osal_mutex_pend(&mu, pdMS_TO_TICKS(TEST_NO_EVENT_MS)));
|
||||
|
||||
/* Once the holder releases it, the same pend succeeds. */
|
||||
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS)));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_pend(&mu, pdMS_TO_TICKS(TEST_WAIT_MS)));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_release(&mu));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_mutex_deinit(&mu));
|
||||
TEST_ASSERT_NULL(mu.mutex);
|
||||
|
||||
vSemaphoreDelete(s_holder_ready);
|
||||
s_holder_ready = NULL;
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
|
||||
static void sem_releaser_task(void *arg)
|
||||
{
|
||||
struct bt_osal_sem *sem = (struct bt_osal_sem *)arg;
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_release(sem));
|
||||
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
TEST_CASE("osal semaphore counts tokens and blocks until released", "[osal]")
|
||||
{
|
||||
struct bt_osal_sem sem = {0};
|
||||
struct bt_osal_sem not_inited = {0};
|
||||
|
||||
osal_test_begin();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_sem_pend(¬_inited, 0));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_sem_release(¬_inited));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_INVALID_PARAM, bt_osal_sem_deinit(¬_inited));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_init(&sem, 2));
|
||||
TEST_ASSERT_NOT_NULL(sem.sem);
|
||||
TEST_ASSERT_EQUAL_UINT16(2, bt_osal_sem_get_count(&sem));
|
||||
|
||||
/* Drain the initial tokens. */
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_pend(&sem, 0));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_pend(&sem, 0));
|
||||
TEST_ASSERT_EQUAL_UINT16(0, bt_osal_sem_get_count(&sem));
|
||||
|
||||
/* No token left: a bounded pend gives up. */
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_TIMEOUT, bt_osal_sem_pend(&sem, pdMS_TO_TICKS(TEST_NO_EVENT_MS)));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_release(&sem));
|
||||
TEST_ASSERT_EQUAL_UINT16(1, bt_osal_sem_get_count(&sem));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_pend(&sem, 0));
|
||||
|
||||
/* A release from another task wakes a blocked pend. */
|
||||
TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(sem_releaser_task, "osal_sem", TEST_HELPER_STACK,
|
||||
&sem, uxTaskPriorityGet(NULL), NULL));
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_pend(&sem, pdMS_TO_TICKS(TEST_WAIT_MS)));
|
||||
TEST_ASSERT_EQUAL_UINT16(0, bt_osal_sem_get_count(&sem));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_sem_deinit(&sem));
|
||||
TEST_ASSERT_NULL(sem.sem);
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
|
||||
TEST_CASE("osal callout posts its event to the event queue on expiry", "[osal]")
|
||||
{
|
||||
struct bt_osal_eventq evq = {0};
|
||||
struct bt_osal_callout co = {0};
|
||||
struct bt_osal_event *ev;
|
||||
bt_osal_time_t ticks;
|
||||
bt_osal_time_t remaining;
|
||||
|
||||
osal_test_begin();
|
||||
|
||||
bt_osal_eventq_init(&evq);
|
||||
TEST_ASSERT_EQUAL_INT(0, bt_osal_callout_init(&co, &evq, ev_cb_count, (void *)0x99));
|
||||
TEST_ASSERT_NOT_NULL(co.co);
|
||||
TEST_ASSERT_FALSE(bt_osal_callout_is_active(&co));
|
||||
|
||||
ticks = bt_osal_time_ms_to_ticks32(200);
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_callout_reset(&co, ticks));
|
||||
|
||||
/* The FreeRTOS timer daemon arms the timer asynchronously, so let it run
|
||||
* before inspecting the callout's state.
|
||||
*/
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
TEST_ASSERT_TRUE(bt_osal_callout_is_active(&co));
|
||||
|
||||
/* An armed callout expires in the future and has time left to run. */
|
||||
TEST_ASSERT_GREATER_THAN_UINT32(bt_osal_time_get(), bt_osal_callout_get_ticks(&co));
|
||||
remaining = bt_osal_callout_remaining_ticks(&co, bt_osal_time_get());
|
||||
TEST_ASSERT_GREATER_THAN_UINT32(0, remaining);
|
||||
TEST_ASSERT_LESS_OR_EQUAL_UINT32(ticks, remaining);
|
||||
|
||||
/* Nothing is delivered before the callout expires. */
|
||||
TEST_ASSERT_NULL(bt_osal_eventq_get(&evq, pdMS_TO_TICKS(TEST_NO_EVENT_MS)));
|
||||
|
||||
/* On expiry the callout's event lands in the queue, ready to be run. */
|
||||
ev = bt_osal_eventq_get(&evq, pdMS_TO_TICKS(TEST_WAIT_MS));
|
||||
TEST_ASSERT_NOT_NULL(ev);
|
||||
bt_osal_event_run(ev);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, s_run_count);
|
||||
TEST_ASSERT_EQUAL_PTR((void *)0x99, s_last_arg);
|
||||
/* A callout is one-shot. */
|
||||
TEST_ASSERT_FALSE(bt_osal_callout_is_active(&co));
|
||||
|
||||
/* Stopping an armed callout keeps its event out of the queue. */
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_callout_reset(&co, bt_osal_time_ms_to_ticks32(100)));
|
||||
bt_osal_callout_stop(&co);
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
TEST_ASSERT_FALSE(bt_osal_callout_is_active(&co));
|
||||
TEST_ASSERT_NULL(bt_osal_eventq_get(&evq, pdMS_TO_TICKS(200)));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, s_run_count);
|
||||
|
||||
bt_osal_callout_deinit(&co);
|
||||
TEST_ASSERT_NULL(co.co);
|
||||
/* Deinit of an already released callout is a no-op. */
|
||||
bt_osal_callout_deinit(&co);
|
||||
|
||||
bt_osal_eventq_deinit(&evq);
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
|
||||
TEST_CASE("osal callout without an event queue runs its handler inline", "[osal]")
|
||||
{
|
||||
struct bt_osal_callout co = {0};
|
||||
|
||||
osal_test_begin();
|
||||
|
||||
/* With no event queue the handler is called straight from timer context. */
|
||||
TEST_ASSERT_EQUAL_INT(0, bt_osal_callout_init(&co, NULL, ev_cb_count, (void *)0x77));
|
||||
|
||||
bt_osal_callout_set_arg(&co, (void *)0x88);
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_callout_reset(&co, bt_osal_time_ms_to_ticks32(50)));
|
||||
|
||||
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS)));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, s_run_count);
|
||||
TEST_ASSERT_EQUAL_PTR((void *)0x88, s_last_arg);
|
||||
|
||||
/* The callout can be re-armed after firing. */
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_callout_reset(&co, bt_osal_time_ms_to_ticks32(50)));
|
||||
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS)));
|
||||
TEST_ASSERT_EQUAL_UINT32(2, s_run_count);
|
||||
|
||||
bt_osal_callout_deinit(&co);
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
|
||||
TEST_CASE("osal time helpers round-trip between ms and ticks", "[osal]")
|
||||
{
|
||||
bt_osal_time_t ticks = 0;
|
||||
uint32_t ms = 0;
|
||||
uint32_t elapsed;
|
||||
uint32_t start;
|
||||
|
||||
osal_test_begin();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_time_ms_to_ticks(1000, &ticks));
|
||||
TEST_ASSERT_GREATER_THAN_UINT32(0, ticks);
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_OK, bt_osal_time_ticks_to_ms(ticks, &ms));
|
||||
TEST_ASSERT_EQUAL_UINT32(1000, ms);
|
||||
|
||||
/* The 32-bit variants must agree with the checked ones. */
|
||||
TEST_ASSERT_EQUAL_UINT32(ticks, bt_osal_time_ms_to_ticks32(1000));
|
||||
TEST_ASSERT_EQUAL_UINT32(1000, bt_osal_time_ticks_to_ms32(ticks));
|
||||
|
||||
#if !BT_OSAL_USE_ESP_TIMER && (configTICK_RATE_HZ < 1000)
|
||||
/* One tick is more than one millisecond here, so a full-range tick count
|
||||
* cannot be expressed in milliseconds.
|
||||
*/
|
||||
TEST_ASSERT_EQUAL_INT(BT_OSAL_EINVAL, bt_osal_time_ticks_to_ms(UINT32_MAX, &ms));
|
||||
#endif
|
||||
|
||||
/* The clock advances by at least the requested delay. */
|
||||
start = bt_osal_time_get();
|
||||
bt_osal_time_delay(bt_osal_time_ms_to_ticks32(50));
|
||||
elapsed = bt_osal_time_get() - start;
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_UINT32(bt_osal_time_ms_to_ticks32(40), elapsed);
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
|
||||
TEST_CASE("osal reports OS state and nests critical sections", "[osal]")
|
||||
{
|
||||
uint32_t outer;
|
||||
uint32_t inner;
|
||||
bool in_outer;
|
||||
bool in_inner;
|
||||
bool after_inner;
|
||||
bool after_outer;
|
||||
|
||||
osal_test_begin();
|
||||
|
||||
TEST_ASSERT_TRUE(bt_osal_os_started());
|
||||
TEST_ASSERT_EQUAL_PTR(xTaskGetCurrentTaskHandle(), bt_osal_get_current_task_id());
|
||||
TEST_ASSERT_EQUAL_UINT32(portMAX_DELAY, BT_OSAL_TIME_FOREVER);
|
||||
TEST_ASSERT_FALSE(bt_osal_hw_is_in_critical());
|
||||
|
||||
/* Sample the state inside the critical section but assert outside it: a failing
|
||||
* assertion longjmps out of the test and would leave the spinlock held.
|
||||
*/
|
||||
outer = bt_osal_hw_enter_critical();
|
||||
in_outer = bt_osal_hw_is_in_critical();
|
||||
inner = bt_osal_hw_enter_critical();
|
||||
in_inner = bt_osal_hw_is_in_critical();
|
||||
bt_osal_hw_exit_critical(inner);
|
||||
after_inner = bt_osal_hw_is_in_critical();
|
||||
bt_osal_hw_exit_critical(outer);
|
||||
after_outer = bt_osal_hw_is_in_critical();
|
||||
|
||||
TEST_ASSERT_TRUE(in_outer);
|
||||
TEST_ASSERT_TRUE(in_inner);
|
||||
/* Leaving the inner section must not clear the outer one. */
|
||||
TEST_ASSERT_TRUE(after_inner);
|
||||
TEST_ASSERT_FALSE(after_outer);
|
||||
|
||||
osal_test_end();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(¶ms);
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
-3
@@ -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
|
||||
|
||||
-3
@@ -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 ".")
|
||||
+67
@@ -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
|
||||
+3
@@ -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
|
||||
+4
@@ -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
|
||||
+6
@@ -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
|
||||
+4
@@ -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
|
||||
+3
@@ -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
|
||||
+3
@@ -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 ".")
|
||||
+23
@@ -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(¶ms, 0, sizeof(params));
|
||||
params.connectable = 1;
|
||||
params.own_addr_type = own_addr_type;
|
||||
params.primary_phy = BLE_HCI_LE_PHY_1M;
|
||||
params.secondary_phy = BLE_HCI_LE_PHY_1M;
|
||||
params.tx_power = 127;
|
||||
params.sid = 1;
|
||||
params.itvl_min = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
|
||||
params.itvl_max = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
|
||||
|
||||
rc = ble_gap_ext_adv_configure(instance, ¶ms, NULL, prph_gap_event, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "ext_adv_configure failed; rc=%d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(sizeof(ext_adv_pattern), 0);
|
||||
if (!data) {
|
||||
ESP_LOGE(TAG, "ext_adv: failed to alloc adv data mbuf");
|
||||
return;
|
||||
}
|
||||
rc = os_mbuf_append(data, ext_adv_pattern, sizeof(ext_adv_pattern));
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "ext_adv: mbuf_append failed; rc=%d", rc);
|
||||
os_mbuf_free_chain(data);
|
||||
return;
|
||||
}
|
||||
|
||||
rc = ble_gap_ext_adv_set_data(instance, data);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "ext_adv_set_data failed; rc=%d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
rc = ble_gap_ext_adv_start(instance, 0, 0);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "ext_adv_start failed; rc=%d", rc);
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "Extended advertising started");
|
||||
}
|
||||
#else
|
||||
static void prph_advertise(void)
|
||||
{
|
||||
struct ble_gap_adv_params adv_params;
|
||||
struct ble_hs_adv_fields fields;
|
||||
int rc;
|
||||
|
||||
memset(&fields, 0, sizeof(fields));
|
||||
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
fields.tx_pwr_lvl_is_present = 1;
|
||||
fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
||||
#if CONFIG_BT_NIMBLE_GAP_SERVICE
|
||||
const char *name = ble_svc_gap_device_name();
|
||||
fields.name = (uint8_t *)name;
|
||||
fields.name_len = strlen(name);
|
||||
fields.name_is_complete = 1;
|
||||
#endif
|
||||
fields.uuids16 = (ble_uuid16_t[]){ BLE_UUID16_INIT(L2CAP_COC_UUID) };
|
||||
fields.num_uuids16 = 1;
|
||||
fields.uuids16_is_complete = 1;
|
||||
|
||||
rc = ble_gap_adv_set_fields(&fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Error setting adv data; rc=%d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&adv_params, 0, sizeof(adv_params));
|
||||
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
|
||||
&adv_params, prph_gap_event, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Error starting adv; rc=%d", rc);
|
||||
}
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_EXTENDED_ADV */
|
||||
|
||||
static void prph_l2cap_coc_mem_init(void)
|
||||
{
|
||||
int rc;
|
||||
rc = os_mempool_init(&sdu_coc_mempool, COC_BUF_COUNT, SDU_BLOCK_SIZE,
|
||||
sdu_coc_mem, "prph_coc_pool");
|
||||
assert(rc == 0);
|
||||
rc = os_mbuf_pool_init(&sdu_os_mbuf_pool, &sdu_coc_mempool, SDU_BLOCK_SIZE,
|
||||
COC_BUF_COUNT);
|
||||
assert(rc == 0);
|
||||
}
|
||||
|
||||
static int prph_l2cap_coc_accept(struct ble_l2cap_chan *chan)
|
||||
{
|
||||
struct os_mbuf *sdu_rx = os_mbuf_get_pkthdr(&sdu_os_mbuf_pool, 0);
|
||||
if (!sdu_rx) {
|
||||
return BLE_HS_ENOMEM;
|
||||
}
|
||||
int rc = ble_l2cap_recv_ready(chan, sdu_rx);
|
||||
if (rc != 0) {
|
||||
os_mbuf_free_chain(sdu_rx);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int prph_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg)
|
||||
{
|
||||
switch (event->type) {
|
||||
case BLE_L2CAP_EVENT_COC_CONNECTED:
|
||||
if (event->connect.status != 0) {
|
||||
ESP_LOGE(TAG, "L2CAP COC connect error: %d", event->connect.status);
|
||||
return 0;
|
||||
}
|
||||
ESP_LOGI(TAG, "L2CAP COC connected, chan=%p", event->connect.chan);
|
||||
coc_chan = event->connect.chan;
|
||||
phy_start_time = 0; /* anchored on first data SDU, not connect */
|
||||
rx_bytes = 0;
|
||||
rx_packets = 0;
|
||||
coc_active = true;
|
||||
phy_name = prph_phy_str(current_phy);
|
||||
return 0;
|
||||
|
||||
case BLE_L2CAP_EVENT_COC_DISCONNECTED:
|
||||
coc_active = false;
|
||||
coc_chan = NULL;
|
||||
current_phy = BLE_HCI_LE_PHY_1M;
|
||||
{
|
||||
int64_t end = esp_timer_get_time();
|
||||
int64_t st = phy_start_time;
|
||||
uint32_t by = rx_bytes;
|
||||
uint32_t pk = rx_packets;
|
||||
prph_report_phy(end, st, by, pk, phy_name);
|
||||
rx_bytes = 0; rx_packets = 0; phy_start_time = 0;
|
||||
}
|
||||
ESP_LOGI(TAG, "L2CAP COC disconnected");
|
||||
return 0;
|
||||
|
||||
case BLE_L2CAP_EVENT_COC_ACCEPT: {
|
||||
/* Pre-grant 2 receive buffers so the central can pipeline 2 SDUs. */
|
||||
int rc = prph_l2cap_coc_accept(event->accept.chan);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
/* Second buffer is best-effort; one buffer is enough for the channel to operate. */
|
||||
if (prph_l2cap_coc_accept(event->accept.chan) != 0) {
|
||||
ESP_LOGW(TAG, "L2CAP COC accept: second RX buffer unavailable, running with one");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case BLE_L2CAP_EVENT_COC_DATA_RECEIVED:
|
||||
if (event->receive.sdu_rx) {
|
||||
if (rx_packets == 0) {
|
||||
phy_start_time = esp_timer_get_time();
|
||||
}
|
||||
rx_bytes += OS_MBUF_PKTLEN(event->receive.sdu_rx);
|
||||
rx_packets += 1;
|
||||
os_mbuf_free_chain(event->receive.sdu_rx);
|
||||
}
|
||||
if (prph_l2cap_coc_accept(event->receive.chan) != 0) {
|
||||
ESP_LOGE(TAG, "DATA_RECEIVED: no mbuf for recv_ready; RX may stall");
|
||||
}
|
||||
return 0;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void prph_stats_task(void *arg)
|
||||
{
|
||||
uint32_t prev_bytes = 0;
|
||||
int64_t prev_time = 0;
|
||||
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
|
||||
if (!coc_active) {
|
||||
prev_bytes = 0;
|
||||
prev_time = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
int64_t now = esp_timer_get_time();
|
||||
uint32_t bytes = rx_bytes;
|
||||
|
||||
if (prev_time > 0) {
|
||||
if (bytes < prev_bytes) {
|
||||
prev_bytes = bytes;
|
||||
prev_time = now;
|
||||
continue;
|
||||
}
|
||||
int64_t dt_us = now - prev_time;
|
||||
uint32_t dt_bytes = bytes - prev_bytes;
|
||||
uint32_t kbps = (uint32_t)((uint64_t)dt_bytes * 8ULL * 1000000ULL
|
||||
/ (uint64_t)dt_us / 1000ULL);
|
||||
ESP_LOGI(TAG, "| RX : %-6" PRIu32 " kbps |", kbps);
|
||||
}
|
||||
|
||||
prev_bytes = bytes;
|
||||
prev_time = now;
|
||||
}
|
||||
}
|
||||
|
||||
static int prph_gap_event(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
if (event->connect.status != 0) {
|
||||
ESP_LOGE(TAG, "Connection failed; status=%d", event->connect.status);
|
||||
prph_advertise();
|
||||
return 0;
|
||||
}
|
||||
ESP_LOGI(TAG, "Connected; handle=%d", event->connect.conn_handle);
|
||||
conn_handle = event->connect.conn_handle;
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
ESP_LOGI(TAG, "Disconnected; reason=%d", event->disconnect.reason);
|
||||
conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
coc_chan = NULL;
|
||||
coc_active = false;
|
||||
current_phy = BLE_HCI_LE_PHY_1M;
|
||||
phy_name = "1M";
|
||||
#if CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
ble_gap_ext_adv_stop(0);
|
||||
#endif
|
||||
prph_advertise();
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE:
|
||||
ESP_LOGI(TAG, "PHY updated: tx=%d rx=%d status=%d",
|
||||
event->phy_updated.tx_phy,
|
||||
event->phy_updated.rx_phy,
|
||||
event->phy_updated.status);
|
||||
if (event->phy_updated.status == 0) {
|
||||
if (coc_active) {
|
||||
int64_t end = esp_timer_get_time();
|
||||
int64_t st = phy_start_time;
|
||||
uint32_t by = rx_bytes;
|
||||
uint32_t pk = rx_packets;
|
||||
prph_report_phy(end, st, by, pk, phy_name);
|
||||
rx_bytes = 0; rx_packets = 0; phy_start_time = 0;
|
||||
}
|
||||
current_phy = event->phy_updated.rx_phy;
|
||||
phy_name = prph_phy_str(event->phy_updated.rx_phy);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_CONN_UPDATE:
|
||||
ESP_LOGI(TAG, "Conn params updated; status=%d", event->conn_update.status);
|
||||
if (event->conn_update.status == 0 && coc_active &&
|
||||
current_phy == BLE_HCI_LE_PHY_CODED) {
|
||||
struct ble_gap_conn_desc desc;
|
||||
if (ble_gap_conn_find(conn_handle, &desc) == 0) {
|
||||
if (desc.conn_itvl != 16 && desc.conn_itvl != 32) {
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(phy_name, "Coded") != 0) {
|
||||
int64_t end = esp_timer_get_time();
|
||||
uint32_t by = rx_bytes;
|
||||
uint32_t pk = rx_packets;
|
||||
prph_report_phy(end, phy_start_time, by, pk, phy_name);
|
||||
rx_bytes = 0; rx_packets = 0; phy_start_time = 0;
|
||||
}
|
||||
phy_name = (desc.conn_itvl >= 32) ? "Coded S8" : "Coded S2";
|
||||
ESP_LOGI(TAG, "Coding scheme updated to %s (CI=%u × 1.25ms)",
|
||||
phy_name, desc.conn_itvl);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_ADV_COMPLETE:
|
||||
#if !CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
prph_advertise();
|
||||
#endif
|
||||
return 0;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void prph_on_reset(int reason)
|
||||
{
|
||||
ESP_LOGE(TAG, "Host reset; reason=%d", reason);
|
||||
}
|
||||
|
||||
static void prph_on_sync(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
rc = ble_hs_util_ensure_addr(0);
|
||||
assert(rc == 0);
|
||||
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
assert(rc == 0);
|
||||
|
||||
rc = ble_l2cap_create_server(L2CAP_COC_PSM, L2CAP_COC_MTU,
|
||||
prph_l2cap_coc_event_cb, NULL);
|
||||
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
||||
ESP_LOGE(TAG, "Failed to create L2CAP COC server; rc=%d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t addr[6] = {0};
|
||||
ble_hs_id_copy_addr(own_addr_type, addr, NULL);
|
||||
ESP_LOGI(TAG, "Device Address: %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
|
||||
|
||||
prph_advertise();
|
||||
}
|
||||
|
||||
static void prph_host_task(void *param)
|
||||
{
|
||||
ESP_LOGI(TAG, "BLE Host Task started");
|
||||
nimble_port_run();
|
||||
nimble_port_freertos_deinit();
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
ret = nimble_port_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "nimble_port_init failed; rc=%d", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
prph_l2cap_coc_mem_init();
|
||||
|
||||
ble_hs_cfg.reset_cb = prph_on_reset;
|
||||
ble_hs_cfg.sync_cb = prph_on_sync;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
|
||||
#if CONFIG_BT_NIMBLE_GAP_SERVICE
|
||||
int rc = ble_svc_gap_device_name_set("l2cap-coc-prph");
|
||||
assert(rc == 0);
|
||||
#endif
|
||||
|
||||
ble_store_config_init();
|
||||
|
||||
if (xTaskCreate(prph_stats_task, "prph_stats", 4096, NULL, 5, NULL) != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create stats task");
|
||||
}
|
||||
|
||||
nimble_port_freertos_init(prph_host_task);
|
||||
}
|
||||
@@ -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
|
||||
+4
@@ -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
|
||||
+7
@@ -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
|
||||
+6
@@ -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
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=30
|
||||
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=30
|
||||
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user