# ESP-IDF Bluetooth LE Audio Architecture

[中文]

This document describes the internal architecture of the ESP-IDF Bluetooth LE Audio stack together with the general-purpose BLE transport it is built on, across two Bluetooth components:

- ESP-BLE-ISO -- a general-purpose BLE transport layer. It provides the GATT, GAP, ISO, L2CAP and HCI primitives, together with the dedicated processing task and locking model that serialize all host events. It is not specific to Bluetooth LE Audio -- it is a self-contained transport that any upper-layer profile can build on, and that can also run on its own: it has its own public API (`esp_ble_iso_*`) and its own initialization, so an application can use it directly without ESP-BLE-AUDIO or any profile layer above it.

- ESP-BLE-AUDIO -- the upper profile layer. It implements the Bluetooth LE Audio profiles and services (PACS, ASCS, BASS, BAP, CAP, VCP, MICP, CSIP, MCP, CCP, TMAP, and so on) and exposes them through the public `esp_ble_audio_*` API.

Note

ESP-BLE-AUDIO is currently the only consumer of ESP-BLE-ISO, but it is not the only intended one, and it is not required at all.

ESP-BLE-ISO is an independent component: it can be built and initialized on its own and driven through its public `esp_ble_iso_*` API for ISO-only use (CIS or BIS without any profile layer) -- see ESP-BLE-ISO Public API.

The transport is also profile-agnostic: its GATT, GAP, ISO, L2CAP and HCI primitives are general BLE building blocks, and other profiles -- for example a future HID-over-ISO -- are expected to build on the same component. Wherever this document describes ESP-BLE-ISO, the behavior applies to any consumer, not only to Bluetooth LE Audio.

Both components run on either of two BLE host stacks -- Bluedroid or NimBLE -- behind a common interface. Making the behavioral differences between the two hosts explicit is a primary goal of this document, because they determine the task context in which callbacks run, the order of operations during connection setup, and how events are dispatched.

Note

This guide serves two audiences. The Overview sections give a layer-level mental model usable by anyone integrating the public API of either component (ESP-BLE-ISO, ESP-BLE-AUDIO). The later sections descend to the implementation level (tasks, queues, locks, per-host dispatch paths) for maintainers working on the components themselves. Source is referenced by file path and symbol name rather than line number, so the references stay valid as the code evolves.

## Architecture Overview

### Layered Stack

ESP-IDF Bluetooth LE Audio is organized as a stack of cooperating layers. Each layer talks only to the layer directly below it, and all host-specific knowledge is confined to the adapter sublayer inside each component.

            flowchart TB
        APP["Application / Examples (app task)"]
        AUDIO["ESP-BLE-AUDIO —<br/>Bluetooth LE Audio profiles and services<br/>(esp_ble_audio_* API)"]
        ISO["ESP-BLE-ISO — transport primitives:<br/>GATT, GAP, ISO, L2CAP, HCI<br/>all serialized by the global ISO lock —<br/>most events run on the ISO task event loop"]
        HOST["BLE Host Stack<br/>(Bluedroid or NimBLE)"]
        CTRL["BLE controller"]
        APP <--> AUDIO
        AUDIO <--> ISO
        ISO <--> HOST
        HOST <--> CTRL
        ISO <-. ISO Data Path .-> CTRL

Note

The solid links are the control path, which follows the layering in both directions -- commands flow down, events flow up. The ISO data path is the exception (the dashed link): it bypasses the host stack in both of the Core Specification's data-path directions. On the Input data path (Host to Controller), ESP-BLE-ISO writes SDUs straight to the controller transport, and the controller reports transmit-complete back through a callback into the ISO task; on the Output data path (Controller to Host), the controller delivers received ISO data through the same kind of callback. Neither path goes through the host stack's HCI event dispatch, which keeps the high-rate audio data path short.

The application calls only the public `esp_ble_audio_*` API (or, for ISO-only use cases, `esp_ble_iso_*`). Everything below the public API is internal and may change between releases.

### Two Components

Component | Responsibilities | Key directories
--- | --- | ---
ESP-BLE-ISO | ACL connection management Advertising and scanning GATT client and server ISO (CIS and BIS) HCI and L2CAP ISO task event loop `esp_ble_iso_*` API | `host/common` `host/adapter/bluedroid` `host/adapter/nimble` `api/include`
ESP-BLE-AUDIO | Bluetooth LE Audio GATT services Profile clients and controllers Object Transfer Service (OTS) `esp_ble_audio_*` API | `host/common` `host/adapter/bluedroid/profiles` `host/adapter/nimble/profiles` `host/services/ots` `api/include`

## Dual-Host Design

Each component is split into a host-agnostic common part and a host-specific adapter part. The directory under `host/common` holds one implementation that does not depend on the host stack, while `host/adapter/bluedroid` and `host/adapter/nimble` hold the code that talks to each host directly. Exactly one adapter is compiled, selected by the active host (`CONFIG_BT_BLUEDROID_ENABLED` or `CONFIG_BT_NIMBLE_ENABLED`). The common layer defines the internal interface that both adapters implement, so the upper layers never branch on the host stack.

The two hosts are not behaviorally identical. They differ in the task their callbacks run on, in how an inbound event reaches the ISO task, and in a number of procedure-level details (MTU exchange, service discovery, ISO setup) that are covered in the per-layer sections. The structural differences are summarized below; the most important one for application developers -- the task a callback runs in -- is detailed in Callback Execution Context.

Host-level differences
Aspect | Bluedroid | NimBLE
--- | --- | ---
Task that raises inbound events | The BTU task (BTA GATT and GAP callbacks run here). | The NimBLE host task.
How the adapter forwards an event | A BTA callback builds an event and posts it to the ISO task queue. | A `*_cb_safe` wrapper takes the ISO lock first; most events are then posted to the ISO task queue.
Synchronous exception | GATT-server attribute access is completed asynchronously: the request is posted to the ISO task, and the response is sent later. | GATT-server attribute access is completed inline on the host task, because NimBLE requires the value to be returned synchronously.
ISO task CPU core | Pinned to the configured Bluedroid core. | Pinned to the configured NimBLE core.
ISO task priority | Matched to the BTU task priority. | Matched to the NimBLE host task priority.

Note

The ISO task is deliberately given the same CPU core and the same priority as the host stack's main task (see `host/common/include/common/task.h`). Running the transport task at the host's priority keeps the two cooperatively scheduled instead of preempting each other.

## Component Internals

The implementation details of each component are listed in the table below, covering tasks, queues, locks, and the per-host dispatch paths, for maintainers working inside the component:

Page | What it covers
--- | ---
ESP-BLE-ISO | concurrency model -- the single ISO task and global lock, and the task each callback runs on connection management advertising, scanning and periodic-advertising sync HCI command path ISO subsystem -- CIS and BIG state machines, and the SDU data path GATT client and server, and the ATT MTU exchange L2CAP (draft) and the application event interface `esp_ble_iso_*` public API
ESP-BLE-AUDIO | open profile adapters and the prebuilt library GATT services and the service-adapter pattern profile clients and controllers object transfer (draft) initialization sequence and ATT-MTU sizing end-to-end unicast and broadcast flows `esp_ble_audio_*` public API

## Cross-Cutting Concerns

This section collects the invariants and practical concerns that span both components and recur throughout the implementation.

### Host Parity

The most important maintenance invariant is that the two host adapters stay behaviorally equivalent. The common layers above them assume identical behavior, so a change to one adapter's connection, cleanup, discovery or event path must be mirrored in the other -- otherwise the upper layers behave differently depending on the host. Where the two genuinely cannot match, the asymmetry is deliberate, and the complete set is small:

Intentional per-host differences
Area | Bluedroid | NimBLE
--- | --- | ---
Task that raises inbound events | BTU task | NimBLE host task
GATT-server attribute access | Posted to the ISO task; response sent asynchronously through BTA | Inline on the host task; value returned synchronously
GATT-client read/write completion | Posted to the ISO task | Inline on the host task
HCI ISO commands | Private direct-HCI path with its own completion semaphore | Typed `ble_hs_hci_*` helpers
GATT service registration | Two-phase and asynchronous (`*_init` then `*_start`) | Single-phase `ble_gatts_add_svcs`
L2CAP connection-oriented channels | Not implemented (TODO) | Implemented (Draft)
Object Transfer Service (OTS) | Not implemented (builds on L2CAP) | Implemented (Draft)
Connection-initiation event routing | Share the engine's BTA GATTC interface (`esp_ble_iso_bluedroid_get_gattc_if`) | Forward GAP events into the engine (`*_gap_app_post_event`)

Any divergence beyond these needs the same kind of justification, recorded at the point where it is introduced.

### Layer Boundaries

The architecture depends on a few boundaries that callers and maintainers should respect:

- The public API is the only stable surface. Everything below the `esp_ble_iso_*` and `esp_ble_audio_*` API is internal and may change between releases.

- The two BLE host stacks are adapted, not modified. The adapter layers exist precisely so the components never change Bluedroid or NimBLE themselves; host behavior is taken as given.

- The profile and controller logic is a prebuilt library with a fixed ABI. The open code interacts with it only through the shared function and configuration tables that are validated at initialization; its internals are not part of this contract.

### Common Pitfalls

The concurrency model in Concurrency and Thread Safety has practical consequences that are easy to get wrong:

- Do not block in a callback. Most callbacks run on the ISO task; blocking there stalls every other event in the stack. Its stack is only 4 KB, so deep call chains and large stack-allocated buffers -- including arrays sized from configuration values -- must be avoided.

- Posted events do not race each other. Because a single task drains the ISO task queues, two events that were both posted cannot run concurrently; reasoning that assumes they can is almost always wrong.

- Mind the callback's task on NimBLE. A GATT read or write completion, or an inbound write to a local service, runs on the NimBLE host task rather than the ISO task; code shared between paths must be correct on both.

- The ISO lock orders access, not completion. Holding it guarantees mutual exclusion, not that an operation you started has finished -- many transport operations complete on a later event.

### Memory Footprint

The open ESP-BLE-ISO and ESP-BLE-AUDIO code has small, deterministic fixed costs:

- The ISO task uses a 4 KB stack and three priority event queues (32 + 64 + 32 entries) drained by one consumer.

- The ISO engine's channel, group and broadcast pools are sized by `CONFIG_BT_ISO_MAX_CHAN`, `CONFIG_BT_ISO_MAX_CIG` and `CONFIG_BT_ISO_MAX_BIG`; the HCI command pool holds a single buffer.

- Per connection, NimBLE caches the discovered attribute database and holds deep-copied parameters for GATT procedures in flight; subscriptions are tracked in per-connection lists on both hosts.

- GATT service attribute tables are allocated at initialization, sized by the services that are enabled.

Because these pools and tables are sized from Kconfig, the open code's footprint is fixed at build time, with no per-stream dynamic growth beyond the SDU buffers in flight. The prebuilt profile-and-controller library is the exception: it allocates its own profile and controller state dynamically -- at initialization and as connections and streams are set up -- so that part of the footprint is not fixed at build time.

## Appendix -- File and Directory Map

### ESP-BLE-ISO

Directory | Contents
--- | ---
`api/include` | The public `esp_ble_iso_*` header.
`host/common` | Host-agnostic transport: connection, advertising, scanning, GATT, HCI, ISO glue, L2CAP, the ISO task and the ISO lock; `app/` holds the application GAP and GATT event surface.
`host/iso` | The ISO engine (CIS and BIG state machines and channel API).
`host/adapter/bluedroid`, `host/adapter/nimble` | Per-host adapters for GAP, GATT, ISO, HCI and (NimBLE) L2CAP.
`host/utils` | Address, UUID, CRC, crypto, timer and buffer helpers.

### ESP-BLE-AUDIO

Directory | Contents
--- | ---
`api/include` | Public `esp_ble_audio_*_api.h` headers, one per profile and service.
`host/common` | Initialization orchestration (`init.c`).
`host/adapter/bluedroid/profiles`, `host/adapter/nimble/profiles` | The GATT service adapters (PACS, ASCS, BASS, CAS, CSIS, HAS, MCS, MICS, TBS, TMAS, VCS).
`host/services/ots` | The Object Transfer Service.
`lib` | The prebuilt per-target profile and controller library.

Was this page helpful?

- Thank you! We received your feedback.
- If you have any comments, fill in Espressif Documentation Feedback Form.

- We value your feedback.
- Let us know how we can improve this page by filling in Espressif Documentation Feedback Form.