feat(bt): Add host-agnostic BT OSAL and shared BLE profile task

- Add bt_osal: event queues, mutexes, semaphores, callouts, etc.
- Add the shared BLE profile task and event queue
- Bring both up and tear them down in the host init/deinit paths
- Add unit tests for the OSAL and the profile task
This commit is contained in:
chenjianhua
2026-08-27 11:33:57 +05:30
committed by Rahul Tank
parent c0168e59bf
commit 59f6ca221b
20 changed files with 3864 additions and 18 deletions
+22
View File
@@ -0,0 +1,22 @@
# BLE profiles: host-agnostic profile cores and the shared profile
# infrastructure they build on (e.g. the shared profile task in common/).
#
set(ble_profiles_srcs "" PARENT_SCOPE)
set(ble_profiles_include_dirs "" PARENT_SCOPE)
if(NOT CONFIG_BT_ENABLED)
return()
endif()
set(_srcs "")
# The header is always visible so profiles can include it unconditionally; the
# implementation is compiled only when the shared task is enabled.
set(_include_dirs "${CMAKE_CURRENT_LIST_DIR}/common/include")
if(CONFIG_BT_PRF_TASK_ENABLED)
list(APPEND _srcs "${CMAKE_CURRENT_LIST_DIR}/common/src/bt_prf_task.c")
endif()
set(ble_profiles_srcs "${_srcs}" PARENT_SCOPE)
set(ble_profiles_include_dirs "${_include_dirs}" PARENT_SCOPE)
+7
View File
@@ -0,0 +1,7 @@
# BLE profiles: aggregation point for the host-agnostic profile cores and the
# shared profile infrastructure they build on. Each profile (and the shared
# infrastructure in common/) contributes its own Kconfig.in here, so the
# top-level bt/Kconfig only ever needs to source this file.
#
source "$IDF_PATH/components/bt/ble_profiles/common/Kconfig.in"
@@ -0,0 +1,45 @@
config BT_PRF_TASK_ENABLED
bool "Enable the shared BLE profile task (event queue worker)"
depends on BT_ENABLED
default n
help
Build the shared BLE profile task: a single process-wide event queue
served by one dedicated FreeRTOS task, onto which BLE profiles post
short, non-blocking handlers instead of each spawning its own task.
Host-agnostic (depends only on the BT OSAL and FreeRTOS), so both the
Bluedroid and NimBLE hosts can use it. A profile that relies on the
shared task should "select BT_PRF_TASK_ENABLED" in its own Kconfig.
config BT_PRF_TASK_STACK_SIZE
int "Shared BLE profile task stack size (bytes)"
depends on BT_PRF_TASK_ENABLED
range 2048 16384
default 3072
help
Stack size, in bytes, of the shared BLE profile task. It must be large
enough for the deepest profile handler posted to the queue; raise it if
a handler needs more stack.
config BT_PRF_TASK_PRIORITY
int "Shared BLE profile task priority"
depends on BT_PRF_TASK_ENABLED
range 1 24
default 5
help
FreeRTOS priority of the shared BLE profile task. Keep it below the BLE
host task so profile handling never starves the protocol stack.
config BT_PRF_TASK_CORE_ID
int "Shared BLE profile task core affinity"
depends on BT_PRF_TASK_ENABLED
range -1 0 if FREERTOS_UNICORE
range -1 1
default 0
help
Core the shared BLE profile task is pinned to: 0 for CPU0, 1 for CPU1,
or -1 to leave it unpinned so the scheduler may run it on either core.
On a single-core target the only valid values are 0 and -1. Pinning the
task to the core that does not run the BLE host can keep profile
handlers from competing with the protocol stack for CPU time.
@@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file bt_prf_task.h
* @brief Shared BLE profile task (event queue worker)
*
* A single, process-wide event queue served by one dedicated FreeRTOS task,
* shared by BLE profiles that need to run their business logic off the host
* task. Profiles post short, non-blocking handlers here instead of each
* spawning its own task, which keeps task count and RAM bounded.
*
* Profiles post work with bt_prf_task_eventq(): obtain the underlying queue and
* drive it directly with a caller-owned ::bt_osal_event embedded in the profile's
* own context. This avoids per-post allocation and lets an in-flight event
* coalesce (an already-queued event is never enqueued twice), and it is the
* queue to bind a ::bt_osal_callout to for delayed work.
*
* Built only when CONFIG_BT_PRF_TASK_ENABLED is set. A profile that relies on
* the shared task should `select BT_PRF_TASK_ENABLED` in its own Kconfig.
*
* @note This is the profile-facing policy layer on top of the OS-primitive OSAL
* (see bt_osal.h). The handler runs in the shared task's context, so it
* MUST be non-blocking and quick; hand long or blocking work off to a
* dedicated task.
*/
#pragma once
#include "bt_osal.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Bring up the shared BLE profile task
*
* @note The active host bring-up already calls this (esp_bluedroid_init() /
* nimble_port_init()), so profiles do NOT need to. Exposed mainly for tests.
*/
bt_osal_error_t bt_prf_task_init(void);
/**
* @brief Tear down the shared BLE profile task
*
* @note The active host tear-down already calls this (esp_bluedroid_deinit() /
* nimble_port_deinit()), so profiles do NOT need to. Exposed mainly for tests.
*/
bt_osal_error_t bt_prf_task_deinit(void);
/**
* @brief Get the shared profile task's event queue
*
* Use it to drive the queue directly with a caller-owned event (zero-allocation
* posting via bt_osal_eventq_put()) or to bind a ::bt_osal_callout for delayed
* work that runs in the shared task's context.
*
* @return Pointer to the shared event queue, or NULL if the task is not running
*/
struct bt_osal_eventq *bt_prf_task_eventq(void);
/**
* @brief Check whether the shared BLE profile task is running
*
* @return true if bt_prf_task_init() has succeeded and the task is up
*/
bool bt_prf_task_is_running(void);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,109 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "bt_osal.h"
#include "bt_osal_freertos.h"
#include "bt_prf_task.h"
static const char *TAG = "bt_prf_task";
/**
* @brief Internal state of the shared BLE profile task
*
* Allocated in bt_prf_task_init() and freed in bt_prf_task_deinit(). The
* pointer @ref s_prf_task is written only during single-threaded bring-up and
* tear-down and read (never written) by bt_prf_task_post(); the documented
* lifecycle contract (init happens-before any post, deinit happens-after the
* last post) makes it safe without a lock.
*/
typedef struct {
struct bt_osal_eventq evq; /*!< Shared event queue served by the worker task */
bool running; /*!< true once the worker task has been started */
} bt_prf_task_ctx_t;
static bt_prf_task_ctx_t *s_prf_task;
bt_osal_error_t bt_prf_task_init(void)
{
bt_prf_task_ctx_t *ctx;
bt_osal_error_t rc;
/* Idempotent: a second bring-up while already running is a no-op. */
if (s_prf_task != NULL) {
return BT_OSAL_OK;
}
ctx = calloc(1, sizeof(*ctx));
if (ctx == NULL) {
ESP_LOGE(TAG, "no memory for profile task context");
return BT_OSAL_ENOMEM;
}
/* Allocates the queue's backing storage; asserts internally on OOM. */
bt_osal_eventq_init(&ctx->evq);
/* Kconfig encodes "any core" as -1; the OSAL wants its own sentinel. */
struct bt_osal_task_info task_info = {
.name = "bt_profiles",
.prio = CONFIG_BT_PRF_TASK_PRIORITY,
.stack_size = CONFIG_BT_PRF_TASK_STACK_SIZE,
.core_id = (CONFIG_BT_PRF_TASK_CORE_ID < 0) ? BT_OSAL_TASK_NO_AFFINITY
: CONFIG_BT_PRF_TASK_CORE_ID,
};
rc = bt_osal_eventq_start(&ctx->evq, &task_info);
if (rc != BT_OSAL_OK) {
ESP_LOGE(TAG, "failed to start profile task: %d", rc);
bt_osal_eventq_deinit(&ctx->evq);
free(ctx);
return rc;
}
ctx->running = true;
s_prf_task = ctx;
ESP_LOGI(TAG, "profile task started (stack %d, prio %d)",
CONFIG_BT_PRF_TASK_STACK_SIZE, CONFIG_BT_PRF_TASK_PRIORITY);
return BT_OSAL_OK;
}
bt_osal_error_t bt_prf_task_deinit(void)
{
bt_prf_task_ctx_t *ctx = s_prf_task;
/* Symmetric no-op if bring-up never happened. */
if (ctx == NULL) {
return BT_OSAL_OK;
}
/* Publish the torn-down state before releasing resources so a stray
* bt_prf_task_post() observes "not running" rather than a dangling queue. */
s_prf_task = NULL;
/* Stops (deletes) the worker task, then frees the queue's storage. */
bt_osal_eventq_deinit(&ctx->evq);
free(ctx);
ESP_LOGI(TAG, "profile task stopped");
return BT_OSAL_OK;
}
struct bt_osal_eventq *bt_prf_task_eventq(void)
{
bt_prf_task_ctx_t *ctx = s_prf_task;
return (ctx != NULL && ctx->running) ? &ctx->evq : NULL;
}
bool bt_prf_task_is_running(void)
{
bt_prf_task_ctx_t *ctx = s_prf_task;
return ctx != NULL && ctx->running;
}