Merge branch 'idf/ble_audio_arch_doc_v6.1' into 'release/v6.1'

docs(ble_audio): Add documents for introducing ISO & LE Audio architectures (v6.1)

See merge request espressif/esp-idf!49536
This commit is contained in:
Jiang Jiang Jian
2026-06-12 17:10:18 +08:00
18 changed files with 3156 additions and 240 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+6 -1
View File
@@ -69,7 +69,12 @@ BLE_ISO_DOCS = [
BLE_AUDIO_DOCS = [
'api-reference/bluetooth/esp-ble-audio.rst',
'api-guides/ble/ble-audio.rst',
'api-guides/esp-ble-audio/ble-audio-index.rst',
'api-guides/esp-ble-audio/ble-audio-introduction.rst',
'api-guides/esp-ble-audio/ble-audio-architecture-overview.rst',
'api-guides/esp-ble-audio/ble-audio-architecture-iso.rst',
'api-guides/esp-ble-audio/ble-audio-architecture-lea.rst',
'api-guides/esp-ble-audio/ble-audio-feature-support-status.rst',
]
CLASSIC_BT_DOCS = [
+1 -1
View File
@@ -46,4 +46,4 @@ Profile
:SOC_BLE_MESH_SUPPORTED: ../esp-ble-mesh/ble-mesh-index
:SOC_BLUFI_SUPPORTED: blufi
:SOC_BLE_AUDIO_SUPPORTED: ble-audio
:SOC_BLE_AUDIO_SUPPORTED: ../esp-ble-audio/ble-audio-index
@@ -0,0 +1,585 @@
.. _arch-iso-transport:
ESP-BLE-ISO
===========
:link_to_translation:`zh_CN:[中文]`
.. |br| raw:: html
<br/>
.. contents:: Table of Contents
:local:
:depth: 2
ESP-BLE-ISO provides every BLE transport primitive an upper-layer profile needs: ACL connection state, advertising and scanning, periodic-advertising sync, the HCI command path, ISO (CIS and BIS), GATT, GAP and L2CAP, and the ISO task event loop and global lock described in :ref:`Concurrency and Thread Safety <arch-concurrency>`. None of it is audio-specific; ESP-IDF Bluetooth LE Audio is simply the profile layer that currently sits on top. This chapter covers the whole component in turn: connection management, advertising and scanning, the HCI command path, the ISO subsystem, GAP, GATT and L2CAP, and the public API.
Two conventions recur throughout the component and are worth stating once:
- **The** ``_safe`` **suffix.** Most internal operations exist as a pair: a core function that assumes the global ISO lock is already held, and a wrapper of the same name with a ``_safe`` suffix that acquires the lock, calls the core function, and releases it. Public entry points and callers from outside the ISO task use the ``_safe`` variant; code already running under the lock (an ISO task handler, or another core function) calls the bare variant directly. This is how the single global lock from :ref:`Protecting Shared State <arch-host-lock>` is applied uniformly without re-entering it accidentally -- although the mutex is recursive, the split keeps the lock boundary explicit at every call site.
- **The** ``bt_le_*`` / ``bt_*`` **naming.** Symbols beginning with ``bt_`` are the host-agnostic interface defined in ``host/common``; symbols beginning with ``bt_le_bluedroid_`` or ``bt_le_nimble_`` are the adapter implementations that exactly one build compiles.
- One family is exempt: the ``hci_le_*`` ISO event handlers in ``host/common`` (for example ``hci_le_biginfo_adv_report``) are named after the HCI event they decode rather than the ``bt_`` interface, and are internal handlers rather than part of the public surface.
.. _arch-concurrency:
Concurrency and Thread Safety
-----------------------------
All ESP-IDF Bluetooth LE Audio processing above the host stack runs on a single task -- the **ISO task** (``iso_task``) -- and all access to shared state is serialized by a single global recursive mutex, the **ISO lock** (``bt_le_host_lock``). These two mechanisms together are what make the stack thread-safe; understanding them is the key to reasoning about ordering and races.
ISO Task Event Loop
~~~~~~~~~~~~~~~~~~~
The ISO task (in ``host/common/task.c``) is a single FreeRTOS task that loops forever, draining work the rest of the stack posts to it with ``bt_le_iso_task_post()``. Each item carries an event-type tag, and the task dispatches it to the handler for that category -- timer, GAP, GATT, ISO HCI, ISO transmit-complete, or ISO receive-data.
Posted work is not one queue but **three priority tiers**, each a separate FreeRTOS queue, joined into one queue set the task blocks on. On each wakeup the task services the tiers **strictly by priority** -- critical first, then normal, then floodable -- processing one item per loop and re-checking the critical tier first on the next:
.. list-table:: ISO task priority tiers
:header-rows: 1
:widths: 16 12 22 50
* - Tier
- Depth
- On overflow
- Events
* - **Critical**
- 32
- drop newest
- ISO receive-data and transmit-complete -- the latency-critical data path, posted non-blocking from the controller task.
* - **Normal**
- 64
- blocks (never dropped)
- Timer, GAP lifecycle, GATT and ISO HCI events -- reliable; the producer blocks until space frees.
* - **Floodable**
- 32
- drop newest
- High-volume best-effort GAP reports -- extended-advertising, periodic-advertising and BIGInfo reports -- posted non-blocking.
The split exists so a burst of GAP reports cannot delay ISO data: the critical tier is always drained first, and the two non-blocking tiers drop their newest item rather than stall their producer when full. Which tier an event lands in is independent of the handler that runs it -- the advertising and periodic-advertising reports on the floodable tier are dispatched by the same ``bt_le_gap_handle_event`` as the normal-tier GAP events, and a floodable BIGInfo report by the same ``bt_le_iso_handle_hci_event`` as a normal ISO HCI event.
.. note::
A single task drains all three tiers, so events are still processed **one at a time and never overlap**, whatever task produced them -- the serialization guarantee the rest of the stack relies on is unchanged. What the tiers change is **ordering**: events are dispatched in priority order, not in the order they were posted, so a higher-priority ISO data event can be serviced ahead of an earlier-posted GAP report. Code that relies on the serialization guarantee stays correct; code that assumed strict first-in, first-out ordering across event categories does not.
A few properties of the task are worth keeping in mind:
- It runs on a **4 KB stack**. Every upper layer, including application and profile callbacks, executes on this stack, so deep call chains and large stack-allocated buffers (for example arrays sized from configuration values) must be avoided.
- Each queued item carries an event-type tag and a heap-allocated payload; the producer allocates the payload and the handler frees it after processing -- except an item dropped on a full non-blocking tier, which the producer frees instead.
- Its CPU core and priority are matched to the active host stack, as described in :ref:`Dual-Host Design <arch-dual-host>`.
An optional **dispatch monitor** (``CONFIG_BT_ISO_DISPATCH_MONITOR``, off by default) times every callback the task dispatches and keeps per-event-type statistics -- a count, the maximum duration, and a slow-count for callbacks exceeding ``CONFIG_BT_ISO_DISPATCH_THRESHOLD_US`` (default 2000 us, about a quarter of a 7.5--10 ms SDU interval). The table is logged periodically (``CONFIG_BT_ISO_DISPATCH_DUMP_PERIOD_S``, default 10 s) and at deinit, to surface a callback that runs long enough to delay the ISO data path. It adds per-dispatch timing overhead and measures wall-clock time, so it is intended for profiling only.
.. _arch-host-lock:
Protecting Shared State
~~~~~~~~~~~~~~~~~~~~~~~
Shared state -- the connection table, the periodic-advertising sync table, the GATT subscription lists, the GATT-server configuration, the ISO bookkeeping -- is reachable from more than one task. The stack does **not** use fine-grained, per-structure locks. Instead, thread safety relies on one coarse-grained **global recursive mutex** -- the **ISO lock**, ``bt_le_host_lock`` / ``bt_le_host_unlock`` (implemented in ``host/common/host.c`` over a Zephyr ``k_mutex``).
The rule is simple and applies everywhere: **any code path that touches shared state acquires this one mutex first.** In practice that means:
- **Public API functions**, called from the application task, take the lock on entry and release it before returning.
- **ISO task event handlers** take the lock around the part of each handler that reads or mutates shared state.
- **NimBLE synchronous callbacks** (subscription changes, received notifications, the GATT-server attribute access path) take the lock before touching shared state.
- **Timer callbacks** take the lock as well.
Because every reader and writer of a given variable holds the same lock, no two tasks can ever touch it concurrently. The mutex is **recursive**, so a handler that already holds the lock can call helper functions that re-acquire it without deadlocking.
If the lock cannot be acquired within a short bounded time, ``bt_le_host_lock`` calls ``abort()`` rather than continuing. A lock that is held that long means the stack is wedged (a deadlock or a stuck callback), which is a programming error rather than a runtime condition to recover from. ``abort()`` is used deliberately instead of ``assert()``, because ``assert()`` becomes a no-op in ``NDEBUG`` builds and would let the caller enter the critical section without the lock held, reintroducing the race. The unlock path additionally verifies that the calling task is the current holder, catching unbalanced unlocks.
.. important::
Two distinct serialization mechanisms are in play, and they cover different paths:
#. The **single consumer** behind the three priority queues serializes everything *posted* to the ISO task (the GAP, GATT, ISO and timer events that originate from the host stack). Posted events are processed one at a time and never overlap, though across tiers they are dispatched in priority order rather than post order.
#. The **global ISO lock** serializes the *other* contexts -- direct application API calls and the NimBLE synchronous callbacks -- against the ISO task handlers.
A bug report that assumes two posted events run concurrently is almost always a false positive: the single consumer rules that out.
.. _arch-callback-context:
Callback Execution Context
~~~~~~~~~~~~~~~~~~~~~~~~~~
A callback that the application registers (a GAP event handler, a GATT attribute handler, a profile event callback) does not always run on the same task, and the task **differs between the two hosts** for one important case. Knowing the context matters: a callback that runs on the ISO task shares its 4 KB stack and must not block it, while a callback that runs on the host task executes synchronously inside the host's attribute-access path.
.. list-table:: Which task a registered callback runs on
:header-rows: 1
:widths: 44 28 28
* - Callback category
- Bluedroid
- NimBLE
* - GAP events (connect, disconnect, security change, PA sync)
- ISO task
- ISO task
* - GATT client notifications and discovery results
- ISO task
- ISO task
* - GATT client read and write completions
- ISO task
- **NimBLE host task**
* - ISO events (connect, data, transmit-complete)
- ISO task
- ISO task
* - Timer callbacks
- ISO task
- ISO task
* - GATT-server attribute read/write and CCC subscription, and any profile callback driven synchronously by an inbound write
- ISO task
- **NimBLE host task**
Two rows place a callback on the NimBLE host task rather than the ISO task. The GATT-client read and write completions land there because NimBLE reports a procedure result through a completion callback on its host task, and the adapter invokes the application callback inline under the ISO lock instead of re-posting it -- whereas Bluedroid posts every BTA GATT event to the ISO task. The GATT-server attribute-access row has a sharper cause: the two hosts complete the access in opposite ways. On Bluedroid the request is posted to the ISO task, the registered callback runs there, and the response is sent **asynchronously** afterwards, as illustrated below:
.. mermaid::
sequenceDiagram
participant P as Peer
participant BTU as BTU task
participant T as ISO task
participant CB as attr read/write callback
P->>BTU: ATT read / write request
BTU->>T: post GATTS event
T->>CB: invoke callback (under ISO lock)
CB-->>T: value / status
T->>BTU: BTA_GATTS_SendRsp (async)
BTU-->>P: ATT response
On NimBLE, by contrast, the host requires the value to be returned **synchronously**, so the callback runs inline on the host task and hands the value straight back:
.. mermaid::
sequenceDiagram
participant P as Peer
participant N as NimBLE host task
participant CB as attr read/write callback
P->>N: ATT read / write request
N->>CB: invoke callback inline (under ISO lock)
CB-->>N: value / status
N-->>P: send response synchronously
.. _arch-event-flow:
Cross-Layer Event Flow
~~~~~~~~~~~~~~~~~~~~~~
The diagram below traces a typical inbound event -- a GAP, GATT-client or ISO event raised by the controller -- from the controller up to the application callback, and shows where the two hosts differ.
.. mermaid::
%%{init: {'sequence': {'noteAlign': 'left'}}}%%
sequenceDiagram
participant C as BLE controller
participant H as Host stack task
participant A as ESP-BLE-ISO adapter
participant T as ISO task
participant CB as App/Profile callbacks
C->>H: HCI event
Note over H: Bluedroid: received on the HCI host task, callback runs on BTU<br/>NimBLE: one host task does both
H->>A: host callback
Note over A: Bluedroid: BTA callback<br/>NimBLE: *_cb_safe (takes ISO lock)
A->>A: build event payload on the heap
A->>T: bt_le_iso_task_post() — enqueue
T->>T: dequeue, take ISO lock
T->>CB: invoke callback in ISO task context
Note over C,T: ISO data / tx-complete is the exception — the controller calls a registered callback directly, bypassing the Host stack
C-->>T: bt_le_iso_task_post() — enqueue (direct callback, on the controller task)
T->>T: dequeue, take ISO lock
T->>CB: channel callback in ISO task context
Where the application and profile callbacks run follows from this flow, and it differs by host:
- On **Bluedroid**, **every upper-layer callback runs in the ISO task context**, whatever triggered it -- GAP, GATT client (notification, discovery, read or write completion), GATT server, ISO or timer. The adapter posts every BTA event to the queue, so there is no exception. These callbacks run on the task's 4 KB stack while the ISO lock is held, and calling a blocking transport API from one stalls the entire event loop.
- On **NimBLE**, the same is true *except* for two categories, which run **inline on the NimBLE host task** rather than the ISO task: **GATT-server attribute access** (the value must be returned synchronously) and **GATT-client read and write completions** (NimBLE reports the result through a host-task completion callback that the adapter invokes directly). A profile callback driven by either -- for example a server-side control-point write handler, or a client read or write completion -- therefore runs on the host task, not the ISO task, and does not pass through the queue. These are the two rows flagged in :ref:`Callback Execution Context <arch-callback-context>`.
Layer and File Map
------------------
The component is organized into a host-agnostic ``host/common`` layer, the two ``host/adapter`` layers, a standalone ISO engine under ``host/iso``, and shared helpers under ``host/utils``. The host-agnostic files are:
.. list-table:: ``host/common`` -- host-agnostic transport
:header-rows: 1
:widths: 24 76
* - File
- Role
* - ``host.c``
- Init and deinit orchestration; the global recursive lock (``bt_le_host_lock`` / ``bt_le_host_unlock``).
* - ``task.c``
- The ISO task event loop, its three priority queues and queue set, and ``bt_le_iso_task_post()``.
* - ``conn.c``
- ACL connection table and the connection-event listener fan-out.
* - ``adv.c``
- Extended-advertising set bookkeeping.
* - ``scan.c``
- Scanning, periodic-advertising sync, and BIGInfo reports.
* - ``hci.c``
- Builds HCI command buffers and dispatches them to the active adapter.
* - ``iso.c``
- Decodes ISO HCI meta-events and bridges the ISO data path to the engine.
* - ``gatt.c``
- Host-agnostic GATT state (subscriptions, attribute database cache).
* - ``l2cap.c``
- Generic L2CAP channel and server dispatch.
* - ``app/gap.c``
- The application-facing GAP entry points.
* - ``app/gatt.c``
- The application-facing GATT entry points.
The ISO state machine lives apart from the rest of the common layer, in ``host/iso/iso.c``: it owns the CIG, BIG and ISO-channel logic and the static, configuration-sized pools that back them. It is the single file under ``host/iso`` and is detailed in :ref:`ISO Subsystem <arch-iso>` below.
Each adapter directory mirrors the common interface for its host: ``host/adapter/bluedroid`` and ``host/adapter/nimble`` each provide ``gap.c``, ``gatt``, ``iso.c`` and the host-specific headers, and ``host/utils`` holds address, UUID, CRC, crypto, timer and buffer helpers that neither layer needs to reimplement.
.. note::
Two files named ``iso.c`` exist and play different roles. ``host/iso/iso.c`` is the engine -- the CIS and BIG state machines and the public ISO channel API. ``host/common/iso.c`` is the glue -- it decodes raw HCI ISO meta-events into the engine's handlers and carries the ISO data tx/rx path. The adapters' ``iso.c`` files translate engine commands into host-native calls.
Connection Management
---------------------
``conn.c`` owns the ACL connection table -- an array of ``bt_conn`` objects keyed by connection handle -- and the registration list for connection-event listeners. The connection lifecycle is driven entirely from the host adapter: when the controller reports a connection event, the adapter calls one of the listener entry points, which update the table and fan the event out to every ``bt_conn_cb`` the upper layers registered with ``bt_conn_cb_register``.
The listener entry points form the host-agnostic seam:
- ``bt_le_acl_conn_connected_listener`` and ``bt_le_acl_conn_disconnected_listener`` add and remove table entries.
- ``bt_le_acl_conn_security_changed_listener``, ``..._identity_resolved_listener``, ``..._pairing_completed_listener`` and ``..._bond_deleted_listener`` carry the security and bonding events.
Because the adapter posts these through the ISO task (see :ref:`Dual-Host Design <arch-dual-host>`), the registered ``bt_conn_cb`` callbacks run in the ISO task context on both hosts -- the GAP-events row of :ref:`Callback Execution Context <arch-callback-context>`. Operations that act on a live connection (``bt_conn_set_security``, ``bt_conn_disconnect``, ``bt_conn_get_info``) and the lookup helpers (``bt_conn_lookup_handle``, ``bt_le_acl_conn_find``, ``bt_conn_foreach``) read from the same table under the ISO lock. The host-specific *sourcing* of these events -- which host task the adapter is on when it calls a listener, and how it maps native event structures -- is covered under :ref:`Application Event Interface <arch-app-event>` below.
Advertising and Scanning
------------------------
``adv.c`` is deliberately small: it tracks extended-advertising *sets* by handle (``bt_le_ext_adv_find`` / ``bt_le_ext_adv_new_safe`` / ``bt_le_ext_adv_delete_safe``). An advertising set is the anchor a broadcast ISO group attaches to, so this table is a prerequisite for the BIG broadcaster flow in the ISO subsystem.
``scan.c`` covers three related responsibilities:
- **Scanning.** The ``bt_le_scan_cb`` registry; advertising reports arrive at ``bt_le_scan_recv_listener`` and are delivered to the application.
- **Periodic-advertising sync.** The sync table (``bt_le_per_adv_sync_new`` / ``..._delete`` / ``..._lookup_addr``) and its listeners -- ``..._establish_listener``, ``..._lost_listener`` and ``..._report_recv_listener`` -- track synchronization to a broadcaster's periodic train.
- **BIGInfo reports.** ``hci_le_biginfo_adv_report`` surfaces the BIGInfo that rides a periodic-advertising train. BIGInfo carries the parameters a receiver needs to synchronize to a Broadcast Isochronous Group, so this handler is the bridge from scanning into the BIG-receiver flow.
Periodic-advertising sync is therefore the entry point for *receiving* broadcast audio: scan, synchronize to the periodic train, read the BIGInfo, and then ask the ISO subsystem to synchronize to the BIG.
HCI Command Path
----------------
The ISO engine never talks to a host stack directly. It builds a standard HCI command buffer with ``bt_hci_cmd_create(opcode, len)`` and submits it with ``bt_hci_cmd_send_sync(opcode, buf, rsp)`` (both in ``host/common/hci.c``). ``bt_hci_cmd_send_sync`` is a one-line dispatcher to the active adapter's ISO command entry point, and this is where the two hosts diverge sharply:
.. list-table:: HCI command translation per host
:header-rows: 1
:widths: 20 40 40
* - Aspect
- Bluedroid
- NimBLE
* - Entry point
- ``bt_le_bluedroid_iso_cmd_send_sync``
- ``bt_le_nimble_iso_cmd_send_sync``
* - Translation
- An opcode switch; each command is forwarded as a raw HCI command over a private *direct-HCI* path.
- An opcode switch; each command is unpacked into NimBLE's typed ``ble_hs_hci_*`` ISO helpers.
* - Synchronization
- A private completion semaphore in ``adapter/bluedroid/hci.c``; the completion callback runs on the HCI-layer task.
- Handled inside the ``ble_hs_hci_*`` call.
The reason Bluedroid carries its own command path is concurrency. Bluedroid's BTU task uses a single global slot to match a synchronous command to its completion; issuing ISO commands from the ISO task through that same slot would race the BTU task. The direct-HCI path in ``adapter/bluedroid/hci.c`` sidesteps this entirely: each command carries its own completion callback and the caller waits on a dedicated semaphore, so the shared BTU slot is never touched. The callback runs on the HCI-layer task and is kept minimal -- it copies the response into a static sink and signals the semaphore, taking no lock. NimBLE needs none of this, because its ``ble_hs_hci_*`` helpers already provide a self-contained synchronous command interface.
.. mermaid::
sequenceDiagram
participant CORE as ISO engine (host/iso/iso.c)
participant HCI as bt_hci_cmd_send_sync (common/hci.c)
participant AD as Active adapter
participant CTRL as Controller
CORE->>HCI: build command buffer (opcode + params)
HCI->>AD: *_iso_cmd_send_sync(opcode)
Note over AD: Bluedroid: direct-HCI (private sem)<br/>NimBLE: ble_hs_hci_* typed helper
AD->>CTRL: HCI command
alt Command Complete (e.g. Set CIG Parameters)
CTRL-->>AD: Command Complete
AD-->>CORE: status + return parameters
else Command Status (e.g. Create CIS)
CTRL-->>AD: Command Status
AD-->>CORE: status (no return parameters)
Note over CORE,CTRL: real outcome arrives later as an LE meta-event<br/>(e.g. CIS Established),<br/>handled in the ISO Subsystem below
end
.. _arch-iso:
ISO Subsystem
-------------
The ISO subsystem is split across three places: the engine (``host/iso/iso.c``), the meta-event and data glue (``host/common/iso.c``), and the adapters (``host/adapter/*/iso.c``). The engine owns three static, configuration-sized pools -- one for ISO channels (``CONFIG_BT_ISO_MAX_CHAN``), one for Connected Isochronous Groups (``CONFIG_BT_ISO_MAX_CIG``), and one for Broadcast Isochronous Groups (``CONFIG_BT_ISO_MAX_BIG``). Every public engine operation follows the ``_safe`` convention from the start of this section.
Inbound ISO meta-events follow the same shape on both hosts: the adapter registers a host-native ISO event callback, packages the event, and posts an ``ISO_HCI_EVENT`` item to the ISO task; ``host/common/iso.c`` then decodes the LE subevent and dispatches it to the engine. One wire-format difference is absorbed here: NimBLE's meta-event structures already include the subevent code, while the Bluedroid adapter prepends it, so the decoder in ``host/common/iso.c`` sees a uniform layout regardless of host.
.. mermaid::
%%{init: {'sequence': {'noteAlign': 'left'}}}%%
sequenceDiagram
participant CTRL as Controller
participant AD as Active adapter
participant T as ISO task
participant CORE as ISO engine
participant APP as App callback
CTRL->>AD: ISO meta-event (HCI)
Note over AD: Bluedroid: BTM ISO callback (BTU task)<br/>NimBLE: ISO callback (host task)
AD->>T: post ISO_HCI_EVENT
CTRL-->>T: ISO data, post ISO_RX_DATA
T->>CORE: handle in common/iso.c, then engine handler
CORE->>APP: channel callback (under ISO lock)
Connected ISO (CIS)
~~~~~~~~~~~~~~~~~~~
A Connected Isochronous Stream is point-to-point and rides an ACL connection. The two roles drive the engine through different entry points:
.. list-table:: CIS roles
:header-rows: 1
:widths: 16 44 40
* - Role
- Flow
- Key symbols
* - Central
- Configure a group, then establish one or more streams to the peers; the controller confirms each stream as it comes up.
- ``bt_iso_cig_create``,\ |br|\ ``bt_iso_cig_reconfigure``,\ |br|\ ``bt_iso_cig_terminate``,\ |br|\ ``bt_iso_chan_connect`` / ``hci_le_cis_established``
* - Peripheral
- Register a server that decides whether to accept incoming streams; each peer request is then accepted or rejected and, if accepted, confirmed.
- ``bt_iso_server_register`` /\ |br|\ ``hci_le_cis_req`` / ``hci_le_cis_established``
Broadcast ISO (BIG)
~~~~~~~~~~~~~~~~~~~
A Broadcast Isochronous Group is connectionless and rides a periodic-advertising train rather than an ACL connection:
.. list-table:: BIG roles
:header-rows: 1
:widths: 16 44 40
* - Role
- Flow
- Key symbols
* - Broadcaster
- Attach a BIG to an extended-advertising set that carries a periodic train; the controller confirms the group, after which the streams can be fed.
- ``bt_iso_big_create`` / ``hci_le_big_complete``,\ |br|\ ``bt_iso_big_terminate`` / ``hci_le_big_terminate``
* - Receiver
- Synchronize to the periodic train, read its BIGInfo, then synchronize to the group; loss of sync is reported back.
- ``bt_iso_big_sync`` / ``hci_le_big_sync_established``,\ |br|\ ``hci_le_big_sync_lost``
The receiver flow begins in ``scan.c``: the BIGInfo delivered by ``hci_le_biginfo_adv_report`` on an established periodic-advertising sync is what ``bt_iso_big_sync`` consumes to join the group.
Data Path and Data Flow
~~~~~~~~~~~~~~~~~~~~~~~
Establishing a CIS or a BIS is not enough to move audio; each stream must be bound to the controller's ISO data path. ``bt_iso_setup_data_path`` binds a direction (input for transmit, output for receive) and ``bt_iso_remove_data_path`` releases it. A stream that is connected but has no data path set up carries no SDUs: establishing the stream and binding its data path are separate steps, so audio does not flow until an upper layer explicitly sets up the path.
Once a data path is set up, SDUs flow through ``host/common/iso.c``:
- **Transmit.** The application submits an SDU with ``bt_iso_chan_send`` (or the timestamped ``bt_iso_chan_send_ts``); the engine hands it to ``bt_le_iso_tx``. If the controller has a free buffer and nothing is queued ahead, the SDU is sent straight to the controller; otherwise it is held in a host-side TX queue. The controller's transmit-complete signal returns as an ``ISO_TX_COMP`` event posted to the ISO task, where ``bt_le_iso_handle_tx_comp`` sends as many queued SDUs as the controller now has free buffers for, then invokes the application's transmit-complete callback.
- **Receive.** An incoming SDU is delivered by the adapter, packaged, and posted as an ``ISO_RX_DATA`` event; ``bt_le_iso_handle_rx_data`` passes it to ``bt_iso_recv`` in the engine, which invokes the channel's receive callback.
Both directions converge on the ISO task: transmit completion and receive delivery are ordinary queued events, so the application's ISO callbacks run in the ISO task context on its 4 KB stack, under the ISO lock, exactly like every other queued event in :ref:`Cross-Layer Event Flow <arch-event-flow>`.
The remaining ESP-BLE-ISO transport primitives are GATT, GAP and L2CAP. As with the rest of the component, the host-agnostic API (``bt_gatt_*``, ``bt_l2cap_*`` and the GAP helpers) lives in ``host/common`` and dispatches to whichever adapter is compiled.
GATT
----
GATT spans a **client** side (this device discovering, reading, writing and subscribing on a peer), a **server** side (this device exposing attributes a peer accesses), and the ATT **MTU exchange** that bounds both. All three are implemented in ``host/common/gatt.c`` over the active adapter.
Client
~~~~~~
The host-agnostic client API in ``host/common/gatt.c`` -- ``bt_gatt_discover``, ``bt_gatt_read``, ``bt_gatt_write``, ``bt_gatt_write_without_response_cb``, ``bt_gatt_subscribe`` and ``bt_gatt_unsubscribe`` -- dispatches to the active adapter's ``bt_le_*_gattc_*`` implementation. The two adapters back the same API with very different machinery:
.. list-table:: GATT client backing per host
:header-rows: 1
:widths: 22 39 39
* - Aspect
- Bluedroid
- NimBLE
* - Discovery
- BTA GATTC procedures; BTA owns the discovered attribute cache.
- A one-time full attribute-table walk cached in ``gatt.db.c``.
* - Procedure serialization
- BTA GATTC's own per-connection queue.
- The NRP queue in ``gatt.nrp.c``; params and data are deep-copied on insert.
* - Result delivery
- Every BTA GATT event is posted to the ISO task.
- Notifications and ``bt_gatt_discover`` results are posted to the ISO task; the peer attribute-table walk and the read and write completions run inline on the host task.
Discovery
^^^^^^^^^
Client discovery has two distinct flows that run on different tasks. The split exists because the hosts differ in whether they keep an attribute cache: Bluedroid's BTA GATTC discovers a peer's attribute table and caches it automatically, whereas NimBLE does not -- so the port adds that cache in ``gatt.db.c``.
**Walking the peer's attribute table (building the cache).** When the upper layer calls ``bt_gattc_disc_start``, the NimBLE adapter (``bt_le_nimble_gattc_db_auto_disc``) runs real ATT discovery against the peer with NimBLE's ``ble_gattc_disc_*`` procedures, walking the whole table once -- every primary service, included service, characteristic and descriptor, including each CCCD. These procedure callbacks run on the **NimBLE host task** and populate the per-connection cached database; the upper layer is notified on the ISO task only once the walk completes. On Bluedroid this step is implicit: ``bt_le_bluedroid_gattc_disc_start`` drives BTA GATTC, which performs the ATT procedures and maintains its own cache.
**Serving a discovery request.** When the upper layer -- for example the audio profiles -- calls ``bt_gatt_discover`` for specific attributes, the NimBLE adapter posts a discovery event to the **ISO task**, where the cached hierarchy answers the request locally with no further ATT traffic and the caller's callback is invoked. On Bluedroid the equivalent results surface from BTA's cache as discovery events on the ISO task.
Subscription Lifecycle
^^^^^^^^^^^^^^^^^^^^^^
Subscriptions are tracked host-agnostically. ``bt_gatt_subscribe`` records each ``bt_gatt_subscribe_params`` on a per-connection list (``gattc_sub``) and, unless an equivalent subscription already exists, writes the CCC through the adapter. Two behaviors are worth noting:
- The subscription is appended to the list *before* the CCC write completes, because some peers send the first notification before replying to the CCC write.
- The ``subscribe`` callback is invoked **synchronously**, completing the procedure in the caller's context rather than after the CCC-write response. On NimBLE this pairs with the cached database: the CCCD handle is already known, so no discovery round-trip is needed and the subscribe path does not block.
On disconnect, ``bt_le_acl_conn_disconnected_gatt_listener`` cleans up in a fixed order: the upper-layer disconnect callbacks run first, then ``gattc_sub_clear`` walks the subscription list, clears each entry's handles and value, and re-initializes the list. Upper layers must therefore **not** zero their own subscribe params inside a disconnect callback -- the params are still linked on the list at that point, and the common layer clears them immediately afterwards.
Procedure Serialization
^^^^^^^^^^^^^^^^^^^^^^^
**Background.** ``gatt.nrp.c`` exists for the same reason as ``gatt.db.c``: it supplies a service BTA GATTC provides natively but NimBLE does not. ATT permits only one outstanding request per connection at a time; BTA GATTC hides that behind its own per-connection procedure queue, whereas NimBLE leaves the caller to honor it.
**What NRP does.** The NRP (need-response PDU) queue in ``gatt.nrp.c`` is the NimBLE-side equivalent: a single per-connection queue spanning every response-bearing PDU -- the client's reads, writes and subscriptions, plus the server's indications (described in the server section below) -- so only one is in flight at a time and the rest wait their turn.
**Enqueue rules.** For the client procedures it bridges to the params-with-callback model the upper layers expect: ``bt_le_nimble_gatt_nrp_insert`` enqueues a read, write or subscribe -- dispatching it at once when the queue is idle, otherwise holding it until the in-flight procedure completes. ``bt_le_nimble_gatt_nrp_remove`` completes the head entry and starts the next; ``bt_le_nimble_gatt_nrp_clear`` discards pending entries on disconnect. Reads come in three forms -- by-UUID, long (fragmented) and single -- each delivering one event per result with explicit end-of-procedure handling.
**Deep copy.** ``bt_le_nimble_gatt_nrp_insert`` deep-copies the params and any data so the caller may pass a stack buffer and free it on return.
**EATT note.** The one-in-flight rule follows ATT's model of a single bearer with one outstanding request per connection. EATT introduces multiple concurrent bearers per connection, so supporting it would mean revisiting the NRP queue to allow per-bearer concurrency rather than a single procedure in flight.
The callback context follows from this. NimBLE delivers a read or write completion on its host task, and the NRP handler invokes the application callback there, under the ISO lock -- the same host-task context as the GATT-server attribute access. Bluedroid instead posts every BTA GATT completion to the ISO task, so the corresponding callbacks run there. This is the read-and-write-completion row of :ref:`Callback Execution Context <arch-callback-context>`.
Server
~~~~~~
The server side of ``host/common/gatt.c`` registers services (``bt_gatt_service_register``), provides the standard attribute read helpers (``bt_gatt_attr_read`` and the service, included-service, characteristic and CCC variants), the CCC write path (``bt_gatt_attr_write_ccc``, ``bt_gatts_sub_changed``) and the outbound notify and indicate API (``bt_gatt_notify_cb``, ``bt_gatt_indicate``). The behavior that differs most between hosts is how an inbound attribute access is completed, the exception first described in :ref:`Callback Execution Context <arch-callback-context>`:
- Bluedroid posts the read or write to the ISO task; the registered attribute callback runs there, and the response is returned to the peer through BTA asynchronously, coordinated with a dedicated server semaphore.
- NimBLE runs the attribute callback inline on the host task, because its access callback must return the value synchronously.
Outbound transfers follow the host's model as well. On NimBLE a notification is a synchronous buffer copy and dispatch, while an indication -- which must await the peer's confirmation -- is queued through NRP and completed when the confirmation arrives. On Bluedroid both go through BTA. In every case the payload handed to notify or indicate is copied before the call returns, so callers may reuse their buffers immediately.
.. _arch-iso-mtu:
MTU Exchange
~~~~~~~~~~~~
The ATT MTU exchange lets the two peers raise the ATT MTU above its 23-octet default so a single PDU can carry a larger attribute value. It is a GATT procedure: the **client** sends the ATT Exchange MTU Request and the **server** responds, and the smaller of the two peers' preferred values becomes the MTU in force. These GATT roles are independent of the GAP central/peripheral role -- a device usually runs both a client and a server regardless of which side opened the connection -- so the behavior is best read per GATT role.
**As a GATT server.** Both hosts always respond to the peer's ATT Exchange MTU Request and accept the negotiated value. This needs nothing from the application and is independent of the GAP role.
**As a GATT client.** Whether the device raises the MTU itself depends on the host stack and, on Bluedroid, on the GAP role:
- On Bluedroid it is automatic: the GATTC adapter issues the exchange when a GATT client connection opens (``handle_gattc_open_event`` calls ``BTA_GATTC_ConfigureMTU``, inside the ``BTA_GATTC_Enh_Open`` path). In practice this fires for a **central**; for a **peripheral** the GATT-client open happens inside the discovery-start path, which is itself gated behind the MTU-updated event -- a circular dependency that keeps it from initiating.
- On NimBLE the component initiates nothing, so the application triggers it with ``ble_gattc_exchange_mtu()`` -- typically right after the security-change event. On Bluedroid that same call is a no-op for the MTU, since the adapter has already exchanged it.
Because a GATT server only ever responds, a single exchange -- initiated by whichever side acts as client first -- settles the MTU for the whole connection; a peer that is also a client on that connection reuses the negotiated value rather than running a second exchange. The preferred value a device offers, and the reason a profile may raise it above the ATT default, are set by the layer above the transport: for Bluetooth LE Audio see :ref:`ATT MTU <arch-audio-mtu>`.
.. _arch-l2cap:
L2CAP (Draft)
-------------
.. warning::
L2CAP connection-oriented channels are an **early draft and are not yet officially supported**. The implementation is incomplete -- it exists only on NimBLE (``host/adapter/nimble/l2cap.c``) and there is **no Bluedroid support** -- and its API and behavior are provisional and may change. Do not rely on this layer in production.
``host/common/l2cap.c`` provides credit-based L2CAP connection-oriented channels: a server registry (``bt_l2cap_server_register``), the outbound channel operations (``bt_l2cap_chan_connect``, ``bt_l2cap_chan_disconnect``, ``bt_l2cap_chan_send``) and the inbound event handlers the host calls when a channel is accepted, connected, disconnected or receives data (``bt_le_l2cap_accept``, ``bt_le_l2cap_connected``, ``bt_le_l2cap_disconnected``, ``bt_le_l2cap_received``). The only consumer in the stack is the Object Transfer Service, which moves bulk objects over a credit-based channel; profiles that do not use object transfer never open one.
.. _arch-app-event:
Application Event Interface
---------------------------
GAP and GATT events do not reach the application straight from the adapter. They pass through a thin application-event interface in ``host/common/app/gap.c`` and ``host/common/app/gatt.c`` -- the single point at which GAP and GATT events leave and enter the application. The paragraphs below follow an inbound event from the adapter up to the application callback, then the reverse injection path.
**Registration.** The application registers exactly one callback per category, through ``bt_le_gap_app_cb_register`` and ``bt_le_gatt_app_cb_register``. Each category keeps a single callback pointer, so there is one application sink for all GAP events and one for all GATT events.
**Sourcing -- adapter to ISO task.** Before the interface can deliver an event, the adapter sources it from the host and posts it to the ISO task queue. For GAP, the Bluedroid adapter registers a BTA/BTM GAP callback in ``bt_le_bluedroid_gap_init``; the callback runs on the BTU task, builds an event, and enqueues it with ``bt_le_bluedroid_gap_post_event``. The NimBLE adapter supplies a ``ble_gap_event`` callback whose ``*_cb_safe`` wrapper takes the ISO lock and posts the event. GATT events are sourced the same way by the GATT adapters, detailed under GATT above. GAP has no engine of its own beyond this -- it is only an *event surface* over state that other modules own: the ACL connection table in ``conn.c`` (ACL connect and disconnect, security and identity changes, bond deletion) and the scanning and periodic-advertising-sync state in ``scan.c`` (extended-scan reports, PA-sync establish, lost and report, BIGInfo).
**Delivery -- ISO task to application.** When the ISO task dequeues a GAP or GATT event it calls ``bt_le_gap_handle_event`` or ``bt_le_gatt_handle_event`` (from ``task.c``). These dispatch on the event type, build a typed ``bt_le_gap_app_event`` or ``bt_le_gatt_app_event``, invoke the registered callback, and free the queued payload. Because this runs inside the ISO task, the application callback executes in the ISO task context under the ISO lock -- the context described in :ref:`Callback Execution Context <arch-callback-context>`. Delivered GAP events include ACL connect and disconnect, security and identity changes, bond deletion, extended-scan reports, periodic-advertising-sync state and BIGInfo; delivered GATT events include the ATT MTU change, GATT-client discovery completion and GATT-server subscription changes.
**Injection -- application to ISO task.** The reverse direction lets the application post an event into the ISO task, through ``bt_le_gap_app_post_event`` (exposed publicly as ``esp_ble_iso_gap_app_post_event``). It is host-divergent: on Bluedroid it forwards to ``bt_le_bluedroid_gap_post_event``, on NimBLE to ``bt_le_nimble_gap_post_event``. In the configuration these components ship, the injection path is used only on NimBLE.
.. mermaid::
flowchart TB
ADP["Adapter<br/>(host events)"]
T["ISO task queue"]
AIF["app/ interface<br/>(gap.c, gatt.c)"]
APP["Application<br/>(one GAP and<br/>one GATT callback)"]
ADP -->|post raw event| T
T -->|dequeue and dispatch| AIF
AIF -->|typed app event| APP
APP -->|app_post_event| AIF
AIF -->|inject into queue| T
**Why the interface is shaped this way.** Three goals drive this design:
- **One event model regardless of host.** The adapter normalizes every native event -- a BTA/BTM callback on Bluedroid, a ``ble_gap_event`` on NimBLE -- into the same typed ``bt_le_gap_app_event`` / ``bt_le_gatt_app_event`` before it reaches the application. The upper layers -- the examples and the audio profiles -- therefore handle an identical set of events and never branch on the host stack. This is the most important property of the interface: moving an application between the two hosts needs no change to its event handling.
- **Internal consumers get the event too.** Several events are not only for the application: the ISO engine in ``host/iso/iso.c`` and the prebuilt audio library consume them as well -- a periodic-advertising sync and its BIGInfo drive a BIG sync, and connect, disconnect and security changes drive the profiles. The interface therefore guarantees a copy reaches the ISO task, where that internal work runs under the ISO lock. On Bluedroid the BTU-task callback posts it directly; on NimBLE the application forwards the event it received into the ISO task with ``esp_ble_iso_gap_app_post_event`` (see :ref:`Per-Host Integration Differences <arch-iso-host-diff>`).
- **Coexistence with an ordinary BLE application.** Posting a copy to the ISO task does not consume the event. On Bluedroid the same callback still forwards the native event to the Bluedroid application-callback layer (BTC), so an application that also registered through ``esp_ble_gap_register_callback`` keeps receiving it for its own, non-audio BLE work; on NimBLE the application is already inside its ``ble_gap_event`` callback and can go on to handle whatever else it needs there. An ESP-IDF Bluetooth LE Audio application and a conventional BLE application can run side by side on one device.
.. important::
These application and profile callbacks run **on the ISO task** (see :ref:`Callback Execution Context <arch-callback-context>`), so a callback must return quickly. Blocking, sleeping, waiting on I/O, or running a long computation in one stalls the single event loop that every other GAP, GATT and -- most critically -- ISO data event depends on, which surfaces directly as late or dropped audio. Hand any lengthy work off to another task.
.. _arch-iso-api:
Public API
----------
Everything described so far is internal. An application sees only one public header, ``api/include/esp_ble_iso_common_api.h``, which exposes the transport as a small ``esp_ble_iso_*`` API for ISO-only use cases -- CIS or BIS without the Bluetooth LE Audio profiles.
Shape and Conventions
~~~~~~~~~~~~~~~~~~~~~
The public API is a thin facade over the ISO engine of the :ref:`transport layer <arch-iso-transport>`:
- **Opaque types.** Public types such as ``esp_ble_iso_chan_t``, ``esp_ble_iso_cig_t`` and ``esp_ble_iso_big_t`` are typedefs of the internal ``bt_iso_*`` structures, and ``esp_ble_conn_t`` is a typedef of ``bt_conn``. The application holds them as opaque handles.
- **Error codes.** Every function returns ``esp_err_t`` rather than the negative ``errno`` values the engine uses internally.
- **Lock on entry.** Each call enters the engine through its ``_safe`` wrapper, so the public API is the outermost point at which the global ISO lock is taken; the application never manages the lock itself.
- **One init.** ``esp_ble_iso_common_init`` takes an ``esp_ble_iso_init_info_t`` whose only field is the application's GAP callback; ISO events are then delivered through that callback or through the per-channel operations.
Functional Groups
~~~~~~~~~~~~~~~~~
.. list-table:: ESP-BLE-ISO public API by purpose
:header-rows: 1
:widths: 24 42 34
* - Group
- Representative functions
- Purpose
* - Initialization
- ``esp_ble_iso_common_init``
- Register the GAP callback and bring up the transport.
* - CIS -- central
- ``esp_ble_iso_cig_create``,\ |br|\ ``esp_ble_iso_cig_reconfigure``,\ |br|\ ``esp_ble_iso_cig_terminate``,\ |br|\ ``esp_ble_iso_chan_connect``
- Configure a Connected Isochronous Group and establish its streams.
* - CIS -- peripheral
- ``esp_ble_iso_server_register``,\ |br|\ ``esp_ble_iso_server_unregister``
- Accept incoming Connected Isochronous Streams.
* - BIG -- broadcaster
- ``esp_ble_iso_big_ext_adv_add``,\ |br|\ ``esp_ble_iso_big_create``,\ |br|\ ``esp_ble_iso_big_terminate``,\ |br|\ ``esp_ble_iso_big_register_cb``
- Broadcast an Isochronous Group over an advertising set.
* - BIG -- receiver
- ``esp_ble_iso_big_sync``
- Synchronize to a broadcast Isochronous Group.
* - Data path
- ``esp_ble_iso_setup_data_path``,\ |br|\ ``esp_ble_iso_remove_data_path``,\ |br|\ ``esp_ble_iso_chan_send``,\ |br|\ ``esp_ble_iso_chan_send_ts``
- Bind a stream to the controller's ISO data path and move SDUs.
* - Information
- ``esp_ble_iso_chan_get_info``,\ |br|\ ``esp_ble_iso_chan_get_tx_sync``
- Query channel and transmit-timing information.
* - Helpers
- ``esp_ble_iso_data_parse``
- Parse length-type-value (LTV) encoded data.
These map one-to-one onto the engine operations in the :ref:`ISO subsystem <arch-iso>`; the public layer adds only the lock, the error translation and the opaque typedefs.
.. _arch-iso-host-diff:
Per-Host Integration Differences
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Two parts of the public API exist specifically because the hosts route connection and GAP events differently. Both solve the same problem -- letting the engine see the events for a connection the application initiated -- in the way each host requires:
- ``esp_ble_iso_gap_app_post_event`` is **needed only on NimBLE**. When the application initiates a connection or scan on NimBLE, the host delivers GAP events to the callback the application registered; the application forwards them into the engine with this function. On Bluedroid the engine installs its own BTM GAP callback and captures the events directly, so no forwarding is required.
- ``esp_ble_iso_bluedroid_get_gattc_if`` is **Bluedroid only**. It returns the engine's internal BTA GATTC interface, which the application passes to ``esp_ble_gattc_open`` when initiating a connection so the resulting ACL events route back to the engine, avoiding a second BTA GATTC registration. The NimBLE counterpart is the event forwarding above.
@@ -0,0 +1,508 @@
.. _arch-audio:
ESP-BLE-AUDIO
=============
:link_to_translation:`zh_CN:[中文]`
.. |br| raw:: html
<br/>
.. contents:: Table of Contents
ESP-BLE-AUDIO sits directly on top of ESP-BLE-ISO and implements the Bluetooth LE Audio profiles and services. It is the layer an ESP-IDF Bluetooth LE Audio application actually programs against, through the ``esp_ble_audio_*`` API.
Component Layout
----------------
The component has two distinct kinds of code, and the distinction matters for this document:
- **Open source, covered here.** The public API headers (``api/include/esp_ble_audio_*_api.h``), the GATT *service* adapters under ``host/adapter/bluedroid/profiles`` and ``host/adapter/nimble/profiles``, the Object Transfer Service under ``host/services/ots``, and the initialization orchestration in ``host/common/init.c``.
- **Prebuilt, out of scope.** The ESP-IDF Bluetooth LE Audio profile and controller *logic* -- the unicast and broadcast clients, the volume, microphone, media and call control, set coordination, and the top-level profiles -- ships as a prebuilt per-target library (``lib/lib/<target>/libble_audio.a``, linked with ``add_prebuilt_library``). This document does not describe its internals; it describes the public contract around it and, where it matters, the task a registered callback runs on.
Layering
--------
.. mermaid::
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 40, 'subGraphTitleMargin': {'top': 6, 'bottom': 14}}}}%%
flowchart TB
APP["Application"]
API["ESP-BLE-AUDIO public API<br/>(esp_ble_audio_*)"]
subgraph AUDIO["ESP-BLE-AUDIO"]
direction LR
LIB["Prebuilt profile and<br/>controller library"]
SVC["GATT service adapters<br/>(per host)"]
OTS["Object Transfer Service"]
LIB ~~~ SVC
LIB ~~~ OTS
end
ISO["ESP-BLE-ISO transport<br/>(GATT, GAP, ISO, L2CAP, ISO task)"]
APP --> API --> AUDIO --> ISO
.. _arch-audio-svc-ctrl:
Services and Controllers
------------------------
Bluetooth LE Audio functionality divides into two kinds of building block, and the component reflects the split:
- **GATT services** are the attribute tables a peer reads, writes and subscribes to. They are implemented in the open per-host adapters (``host/adapter/*/profiles``), one source file per service, on top of the ESP-BLE-ISO GATT-server layer. Grouped by their profile, as in the Bluetooth LE Audio specification, the set is PACS, ASCS and BASS (BAP), MCS (MCP), TBS (CCP), CSIS (CSIP), MICS (MICP), VCS (VCP), CAS (CAP), TMAS (TMAP) and HAS (HAP).
- **Profile clients and controllers** are the state machines that drive those services and orchestrate streams: the Basic Audio and Common Audio profiles, volume, microphone, media and call control, coordinated-set identification, and the top-level Telephony-and-Media, Gaming, and Public-Broadcast profiles. This logic lives in the prebuilt library; the application reaches it through the matching ``esp_ble_audio_*_api.h`` header.
The GATT services are detailed in :ref:`GATT Services <arch-audio-services>`, and the clients and controllers in :ref:`Clients and Controllers <arch-audio-profiles>`.
Where Profile Callbacks Run
---------------------------
A registered ESP-IDF Bluetooth LE Audio callback runs on whichever task delivered the underlying transport event, so the rules from :ref:`Callback Execution Context <arch-callback-context>` carry over directly:
- A callback driven by a **notification** -- for example an ASCS or volume-state change arriving as a GATT notification -- runs on the ISO task on both hosts.
- A callback driven by a **GATT client read or write completion** -- for example reading a peer's PAC records -- runs on the ISO task on Bluedroid but on the **NimBLE host task** on NimBLE.
- A callback driven by an inbound **attribute write to a local service** -- for example a client configuring an ASE -- runs on the ISO task on Bluedroid but on the **NimBLE host task** on NimBLE.
The consequence is the one that runs through the whole stack: on the ISO task a callback shares the 4 KB stack and must not block the event loop, while on the NimBLE host task it executes inside the host's attribute path. In every case the global ISO lock is held, so profile state stays consistent regardless of which task the callback runs on.
.. _arch-audio-services:
GATT Services
-------------
Bluetooth LE Audio exposes its capabilities and state through a fixed set of GATT services. These are the *server* side of the profiles -- the attribute tables a peer reads, writes and subscribes to -- and, as established in :ref:`Services and Controllers <arch-audio-svc-ctrl>`, they are the open part of ESP-BLE-AUDIO: one source file per service under ``host/adapter/bluedroid/profiles`` and ``host/adapter/nimble/profiles``. The state behind each service, and the command handling for its control points, lives in the prebuilt library; this section covers the services and their adapters, not that logic.
.. _arch-audio-adapter:
Service Adapter Pattern
~~~~~~~~~~~~~~~~~~~~~~~
Every service is realized the same way. The prebuilt library owns the canonical, host-agnostic definition of the service's attribute table (a ``bt_gatt_service``); the open adapter registers that table with the active host's GATT server and then reconciles the handles the host assigns back onto the library's definition, so the library can address its own attributes when it notifies or indicates. The two hosts differ in how the registration is done:
- The **NimBLE** adapter declares the service a second time in NimBLE's native form (a ``ble_gatt_svc_def``), registers it with ``ble_gatts_add_svcs``, checks that its characteristics match the library's definition, and maps the assigned handles back with a ``*_attr_handle_set`` step. Registration is synchronous and single-phase.
- The **Bluedroid** adapter hands the library's ``bt_gatt_service`` to a shared ``bt_le_bluedroid_svc_init`` helper that drives BTA GATTS. Because BTA registers a service in two asynchronous phases, every Bluedroid service adapter exposes a matching ``*_init`` and ``*_start`` pair and coordinates completion with the server semaphore from the GATT-server layer.
Once a service is registered, peer access to its attributes follows the per-host attribute-access path of :ref:`Callback Execution Context <arch-callback-context>`: completed synchronously on the NimBLE host task, or posted to the ISO task on Bluedroid.
.. _arch-audio-deferred-add:
**When a service is added (deferred registration)**
**Mechanism.** Services are not all added to the GATT table during ``esp_ble_audio_common_init``. The build defines ``BLE_AUDIO_SVC_DEFERRED_ADD`` by default, which holds a service back until the application registers the matching role through its ``esp_ble_audio_<profile>_register`` call -- the prebuilt library turns that call into the service's ``bt_le_<service>_init`` registration -- rather than adding it during init.
**Reason.** A firmware image usually has more Bluetooth LE Audio capabilities compiled in than any one application uses: enabling a service's Kconfig role option builds its code in, but the application may never take that role. If every compiled-in service were added at init, a connecting peer would discover and could access attributes for a service that has no backing state behind it yet -- the application has not registered it, so the prebuilt library cannot answer for it meaningfully.
**Impact on the application.** Deferring the add keeps an unused-but-compiled-in capability out of the GATT table until the application opts into it, so a peer only ever discovers services that are fully backed.
Service Reference
~~~~~~~~~~~~~~~~~
.. list-table:: Bluetooth LE Audio GATT services
:header-rows: 1
:widths: 10 24 38 11 17
* - Service
- Full name
- Key characteristics
- Adapter
- Profile
* - PACS
- Published Audio Capabilities Service
- Sink and Source PAC records, audio locations, available and supported audio contexts.
- ``pacs.c``
- BAP
* - ASCS
- Audio Stream Control Service
- Sink and Source Audio Stream Endpoints (ASEs) and the ASE control point.
- ``ascs.c``
- BAP
* - BASS
- Broadcast Audio Scan Service
- Broadcast Receive State and the broadcast audio scan control point.
- ``bass.c``
- BAP
* - MCS
- Media Control Service
- Media player name and track information, the media control point and related state.
- ``mcs.c``
- MCP
* - TBS
- Telephone Bearer Service
- Bearer information, call states and the call control point.
- ``tbs.c``
- CCP
* - CSIS
- Coordinated Set Identification Service
- Set Identity Resolving Key (SIRK), set size, set member lock and rank.
- ``csis.c``
- CSIP
* - MICS
- Microphone Control Service
- Microphone mute; includes AICS.
- ``mics.c``
- MICP
* - VCS
- Volume Control Service
- Volume state, the volume control point and volume flags; includes VOCS and AICS.
- ``vcs.c``
- VCP
* - CAS
- Common Audio Service
- Identifies a Common Audio device; may include CSIS.
- ``cas.c``
- CAP
* - TMAS
- Telephony and Media Audio Service
- The device's TMAP role.
- ``tmas.c``
- TMAP
* - HAS
- Hearing Access Service
- Hearing-aid features, the preset control point and the active preset index.
- ``has.c``
- HAP
Each service is configured by the application through its profile's ``esp_ble_audio_*_api.h`` header (for example ``esp_ble_audio_vcp_api.h`` for VCS, ``esp_ble_audio_pacs_api.h`` for PACS); those APIs are part of the client-and-controller reference that follows.
Included Services and Control Points
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Several services are not standalone but are *included* by another service:
- **CAS** includes **CSIS** when the device is a member of a coordinated set, so a peer discovers set membership through the common audio device.
- **VCS** includes zero or more **VOCS** (Volume Offset Control Service) and **AICS** (Audio Input Control Service) instances -- one VOCS per independently offset output, one AICS per audio input.
- **MICS** includes zero or more **AICS** instances, one per audio input.
AICS and VOCS therefore have public API headers (``esp_ble_audio_aics_api.h`` and ``esp_ble_audio_vocs_api.h``) but no standalone adapter file; the including service registers them as part of its own attribute table.
Several services -- ASCS, BASS, MCS, TBS, CSIS, VCS and HAS -- expose a *control point*: a writable characteristic that carries a command and is answered by notifying the affected state characteristic. The command handling lives in the prebuilt library; the adapter only carries the write inward and the resulting notification outward, on the task described in :ref:`Callback Execution Context <arch-callback-context>`.
.. _arch-audio-profiles:
Clients and Controllers
-----------------------
The profiles and their client and controller roles are the part of ESP-BLE-AUDIO that ships in the prebuilt library. This section describes them at the level this document can: their roles, the public API through which an application drives them, and the task their callbacks run on. The internal state machines are out of scope.
A *client* or *controller* role acts on a remote device over a connection -- for example a volume controller changing a renderer's volume -- while a *renderer*, *device*, *member* or *server* role responds locally and is backed by one of the GATT services in the previous section. A single application can hold several roles at once.
Profile Relationships
~~~~~~~~~~~~~~~~~~~~~
Bluetooth LE Audio profiles build on one another. The **Basic Audio Profile (BAP)** is the foundation: it establishes and carries the audio streams, both unicast (over Connected Isochronous Streams) and broadcast (over Broadcast Isochronous Streams). The **control profiles** -- volume, microphone, coordinated-set, media and call control -- manage features and are each backed by a GATT service. The **Common Audio Profile (CAP)** coordinates the others so an operation applies consistently across every member of a set. The **top-level profiles** -- Telephony and Media Audio (TMAP), Gaming Audio (GMAP), Public Broadcast (PBP) and Hearing Access (HAP) -- are defined combinations of the layers below.
.. mermaid::
flowchart TB
TOP["Top-level profiles — TMAP, GMAP, PBP, HAP"]
CAP["CAP — coordinated control across a set"]
CTRL["Control profiles — VCP, MICP, CSIP, MCP, CCP"]
BAP["BAP — unicast (CIS) and broadcast (BIS) streams"]
SVC["Bluetooth LE Audio GATT services"]
ISO["ESP-BLE-ISO transport"]
TOP --> CAP
CAP --> BAP
CAP --> CTRL
TOP --> CTRL
CTRL --> SVC
BAP --> ISO
Profile Reference
~~~~~~~~~~~~~~~~~
.. list-table:: Bluetooth LE Audio profiles
:header-rows: 1
:widths: 10 27 45 18
* - Profile
- Full name
- Roles
- API header
* - BAP
- Basic Audio Profile
- Unicast client and server, broadcast source and sink, broadcast assistant, scan delegator.
- ``bap_api.h``
* - CAP
- Common Audio Profile
- Initiator, commander and handover across a coordinated set.
- ``cap_api.h``
* - VCP
- Volume Control Profile
- Volume controller (client) and volume renderer (server).
- ``vcp_api.h``
* - MICP
- Microphone Control Profile
- Microphone controller (client) and microphone device (server).
- ``micp_api.h``
* - CSIP
- Coordinated Set Identification Profile
- Set coordinator (client) and set member (server).
- ``csip_api.h``
* - MCP
- Media Control Profile
- Media Control Client (client) and the media proxy (server).
- ``mcc_api.h``, ``media_proxy_api.h``
* - CCP
- Call Control Profile
- Call Control Client (the server side is the Telephone Bearer Service).
- ``ccp_api.h``
* - TMAP
- Telephony and Media Audio Profile
- Role configuration combining CAP, BAP and the control profiles.
- ``tmap_api.h``
* - GMAP
- Gaming Audio Profile
- Role configuration for low-latency gaming audio.
- ``gmap_api.h``
* - PBP
- Public Broadcast Profile
- Public broadcast announcement helpers over BAP broadcast.
- ``pbp_api.h``
API Shape and Roles
~~~~~~~~~~~~~~~~~~~
Each profile exposes a small, uniform API in its ``esp_ble_audio_<profile>_api.h`` header: the application registers a callback structure for the role it is taking and calls functions to start operations -- discover a peer, configure a stream, set a volume, place a call. Results and state changes then arrive asynchronously through that callback, on the task described in :ref:`Callback Execution Context <arch-callback-context>` -- the ISO task in most cases, the NimBLE host task for GATT read and write completions and for inbound writes to a local service.
Because registration and dispatch are uniform across profiles, adding a role to an application always has the same shape: enable the role's Kconfig option, register its callback structure, and drive it through its functions. The role options also determine which GATT services are compiled in, as the initialization flow below shows.
Object Transfer (Draft)
-----------------------
.. warning::
Object Transfer is an **early draft and is not yet officially supported**. It builds on the draft L2CAP channel (:ref:`L2CAP <arch-l2cap>`), so it runs only on NimBLE and **not on Bluedroid**, and its API and behavior are provisional and may change. Do not rely on it in production.
The Object Transfer Service (OTS) moves bulk objects -- larger than an ordinary GATT read can carry -- over the credit-based L2CAP channel from :ref:`L2CAP <arch-l2cap>`. In Bluetooth LE Audio it is used by Media Control: a media player exposes objects such as the current track segmentation and group structure, and a client transfers them with OTS, reached through the media API (``esp_ble_audio_mcs_get_ots`` and the Media Control Client).
Unlike the profiles, OTS is open source, under ``host/services/ots``. Its parts are:
- a **server** (``ots.c``) and a **client** (``ots_client.c``);
- the **Object Action Control Point** (``ots_oacp.c``), which carries the read, write, create and similar object operations;
- the **Object List Control Point** (``ots_olcp.c``), which navigates the list of objects;
- an **object manager** (``ots_obj_manager.c``) and a **directory listing** object (``ots_dir_list.c``);
- the **L2CAP transport** (``ots_l2cap.c``) that carries object data over the credit-based channel.
.. _arch-init-flow:
Initialization Flow
-------------------
An ESP-IDF Bluetooth LE Audio application brings the stack up with two calls, mirroring the transport: ``esp_ble_audio_common_init`` followed by ``esp_ble_audio_common_start``.
``esp_ble_audio_common_init`` runs ``bt_le_audio_init`` in ``init.c``, which first establishes the boundary with the prebuilt library -- it checks that the shared-structure ABI matches and pushes the active Kconfig values into the library -- and then calls the host-specific ``bt_le_{host}_audio_init``. The host initializer brings up the standard GAP and GATT services and then registers each enabled Bluetooth LE Audio service through its adapter, in a fixed order: PACS, ASCS, BASS, TMAS, GTBS, HAS, CSIS and CAS, then the media, volume and microphone services. Each service is compiled in only when its Kconfig role option is set, so a build contains exactly the services its roles need. That fixed order is the order the services register in; *when* most of them actually enter the GATT table is a separate question -- by default the add is deferred to the application's per-role ``esp_ble_audio_<profile>_register`` call rather than happening here at init (see :ref:`deferred registration <arch-audio-deferred-add>`).
``esp_ble_audio_common_start`` runs ``bt_le_audio_start`` and dispatches to the host-specific start, which first registers the coordinated-set services (CSIS and CAS) from ``start_info`` and then commits the GATT server. On Bluedroid the commit is the second phase that actually starts the BTA services registered during init -- the ``*_init`` and ``*_start`` pairing from :ref:`The Service Adapter Pattern <arch-audio-adapter>`; on NimBLE it calls ``ble_gatts_start`` and then reconciles the assigned attribute handles back into the library.
.. mermaid::
%%{init: {'sequence': {'noteAlign': 'left'}}}%%
sequenceDiagram
participant APP as Application
participant CMN as ESP-BLE-AUDIO (init.c)
participant LIB as Prebuilt library
participant AD as Host adapter
APP->>CMN: esp_ble_audio_common_init (gap_cb, gatt_cb)
CMN->>LIB: check ABI, push configuration
CMN->>AD: bt_le_{host}_audio_init
Note over AD: bring up GAP/GATT, then register<br/>enabled services in fixed order
APP->>CMN: esp_ble_audio_common_start
CMN->>AD: bt_le_{host}_audio_start
Note over AD: register CSIS/CAS from start_info, then commit the GATT server<br/>Bluedroid: phase-2 start of the registered BTA services<br/>NimBLE: ble_gatts_start, then reconcile attr handles
.. _arch-audio-mtu:
ATT MTU
~~~~~~~
Bluetooth LE Audio depends on an ATT MTU large enough to carry its control PDUs.
**Spec floor.** The Basic Audio Profile (BAP) requires a minimum ATT MTU of only 64 octets (the public ``ESP_BLE_AUDIO_ATT_MTU_MIN``).
**Why 128.** 64 octets is not enough to operate four ASEs at once, so ESP-IDF raises the default to 128 octets (``BLE_AUDIO_ATT_MTU_MIN``) to leave headroom for the ASCS and PACS control PDUs that Bluetooth LE Audio relies on. Both hosts set the device's preferred ATT MTU to this value during initialization (``bt_le_{host}_audio_init``), through ``BTA_GATT_SetLocalMTU`` on Bluedroid and ``ble_att_set_preferred_mtu`` on NimBLE.
**Negotiation rule.** This is the value the device offers as a client and accepts as a server, so the MTU that ends up in force is the smaller of the two peers' preferred values.
The exchange itself is the GATT procedure described in :ref:`MTU Exchange <arch-iso-mtu>`: the client sends the request, the server responds, and one negotiation governs the whole connection. In the common Bluetooth LE Audio topology -- a phone as central and an ESP device as peripheral -- each device runs **both** a GATT client and a GATT server, since the GATT roles are independent of the GAP role. Yet a single MTU exchange covers the whole connection: the phone's client initiates it, the ESP's server responds, and the one negotiated value then governs all ATT traffic between them -- no matter which side is acting as client or server at a given moment:
.. mermaid::
sequenceDiagram
box transparent Phone (GAP central)
participant PS as GATT server
participant PC as GATT client
end
box transparent ESP (GAP peripheral)
participant ES as GATT server
participant EC as GATT client
end
PC->>ES: ATT Exchange MTU Request
ES-->>PC: ATT Exchange MTU Response
Note over ES: server responds, never initiates
Note over PS,EC: the one negotiated MTU (min of both sides)<br/>governs all ATT traffic, both directions
PC->>ES: service discovery
PC->>ES: subscribe (CCCD), then PACS / ASCS reads, writes
EC->>PS: service discovery (peripheral as client, reuses the same MTU)
The other two roles -- the phone's GATT server and the ESP's GATT client -- are present on the same connection: the ESP's client can run its own service discovery against the phone's server, but neither role runs a second MTU exchange; both reuse the value the phone's client already negotiated. Because the ESP peripheral only responds and never initiates the MTU exchange, that one negotiation is paced by the central, not by the peripheral's host stack.
End-to-End Flows
----------------
The four flows below tie the layers together for the two unicast roles and the two broadcast roles. They are deliberately high-level -- each arrow may stand for several exchanges -- and show which component owns each step.
Unicast Initiator
~~~~~~~~~~~~~~~~~
A unicast initiator connects to an acceptor, configures its audio stream endpoints, establishes a Connected Isochronous Stream and starts sending audio:
.. mermaid::
sequenceDiagram
participant APP as Application (initiator)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>ISO: scan and connect (GAP)
APP->>ISO: start GATT discovery
AUD->>ISO: discover PACS and ASCS, subscribe to ASEs
AUD->>ISO: configure ASEs (write the ASE control point)
AUD->>ISO: create CIG and connect CIS
ISO->>CTRL: LE Set CIG Parameters, Create CIS
CTRL->>ISO: CIS Established event
AUD->>ISO: set up the ISO data path (input)
APP->>CTRL: send ISO SDUs
CTRL->>ISO: ISO transmit complete
Note over APP,CTRL: subsequent SDUs repeat the send / transmit-complete cycle
The stream setup -- discovering PACS and ASCS, configuring the ASEs, creating the CIG, connecting the CIS and setting up the data path -- is driven by the ESP-BLE-AUDIO profile library on top of ESP-BLE-ISO's GATT and ISO layers, not by the application directly. The application drives only the high-level flow through the ``esp_ble_audio_*`` API -- connect, start discovery, start the stream and send audio -- and ESP-BLE-AUDIO carries out the underlying ESP-BLE-ISO operations on its behalf.
Unicast Acceptor
~~~~~~~~~~~~~~~~
A unicast acceptor advertises, accepts a connection from an initiator and lets the initiator configure its audio stream endpoints before receiving audio:
.. mermaid::
sequenceDiagram
participant APP as Application (acceptor)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>ISO: advertise (GAP, audio announcement)
CTRL->>ISO: ACL connection established (peripheral)
CTRL->>ISO: remote initiator writes the ASE control point
ISO->>AUD: ASCS server applies config / QoS / enable
AUD->>APP: config / QoS / enable callbacks
APP->>AUD: response (accept / reject + QoS preferences)
AUD->>ISO: notify ASE state and control-point result
ISO->>CTRL: send to the remote initiator
CTRL->>ISO: CIS Established event
AUD->>ISO: set up the ISO data path (output)
CTRL->>ISO: ISO SDUs
ISO->>APP: recv callback on the ISO task
Note over APP,CTRL: subsequent ISO SDUs repeat the receive / recv-callback cycle
Unlike the initiator, the acceptor is passive. The remote initiator drives the ASCS state machine by writing the ASE control point, and the acceptor's ESP-BLE-AUDIO ASCS server surfaces each step -- config, QoS, enable -- to the application as callbacks. Each callback returns the application's response -- accept or reject, plus QoS preferences at codec configuration -- which the ASCS server notifies back to the initiator as the updated ASE state and control-point result. The Connected Isochronous Stream is established by the initiator (the acceptor is the peripheral); ESP-BLE-AUDIO sets up the ISO data path and delivers the received SDUs to the application through the recv callback on the ISO task. As on the initiator, the application never calls the ESP-BLE-ISO ISO API directly.
Broadcast Source
~~~~~~~~~~~~~~~~
A broadcast source configures its audio, starts a periodic advertising train carrying the BASE and transmits audio into a Broadcast Isochronous Group without any connection:
.. mermaid::
sequenceDiagram
participant APP as Application (broadcast source)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>AUD: create broadcast source (codec, QoS, BASE)
APP->>ISO: start extended and periodic advertising (GAP, carries the BASE)
APP->>AUD: start the broadcast source
AUD->>ISO: create BIG
ISO->>CTRL: LE Create BIG
CTRL->>ISO: BIG Complete event
AUD->>ISO: set up the ISO data path (input)
AUD->>APP: stream started callback
APP->>CTRL: send ISO SDUs
CTRL->>ISO: ISO transmit complete
Note over APP,CTRL: subsequent SDUs repeat the send / transmit-complete cycle
The application drives the high-level flow through the ``esp_ble_audio_*`` API -- create the broadcast source, start advertising, start the source and send audio -- while ESP-BLE-AUDIO builds the BASE, creates the BIG on top of ESP-BLE-ISO's ISO subsystem and sets up the data path. As with the unicast initiator, the underlying ESP-BLE-ISO operations are carried out by ESP-BLE-AUDIO, not the application. A broadcast source has no connection and no GATT: it only advertises and transmits.
Broadcast Sink
~~~~~~~~~~~~~~
A broadcast sink synchronizes to a broadcaster's periodic train, joins its Broadcast Isochronous Group and receives audio:
.. mermaid::
sequenceDiagram
participant APP as Application (broadcast sink)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>ISO: scan and synchronize to periodic advertising
CTRL->>ISO: BIGInfo report
AUD->>ISO: BIG sync (with broadcast code if encrypted)
ISO->>CTRL: LE BIG Create Sync
CTRL->>ISO: BIG Sync Established event
AUD->>ISO: set up the ISO data path (output)
CTRL->>ISO: ISO SDUs
ISO->>APP: recv callback on the ISO task
Note over APP,CTRL: subsequent ISO SDUs repeat the receive / recv-callback cycle
The application drives the high level -- periodic-advertising synchronization (through GAP) and the broadcast-sink start -- while the ESP-BLE-AUDIO broadcast-sink profile performs the BIG sync and the data path. Both rest on ESP-BLE-ISO: its scanning layer for PA sync and the BIGInfo report, its ISO subsystem for the BIG sync and data path. If the group is encrypted and the broadcast code is wrong, the controller still synchronizes but the stream fails its message-integrity check shortly afterward, so an application should treat an early post-sync failure as a likely bad code.
.. _arch-audio-api:
Public API
----------
The application drives the ESP-IDF Bluetooth LE Audio implementation through the ``esp_ble_audio_*`` API. It has two surfaces: one common header, ``api/include/esp_ble_audio_common_api.h``, for bring-up and the cross-cutting entry points, and one ``<profile>_api.h`` header per profile and service for the role-specific operations -- the bulk of the API, cataloged in :ref:`Clients and Controllers <arch-audio-profiles>`.
Shape and Conventions
~~~~~~~~~~~~~~~~~~~~~
The common API sits on the ESP-BLE-ISO :ref:`transport <arch-iso-transport>` and the prebuilt profile library, and follows the same conventions as the transport's public API:
- **Two-call bring-up.** ``esp_ble_audio_common_init`` takes an ``esp_ble_audio_init_info_t`` -- a GAP callback and a GATT callback, the single application sinks of :ref:`Application Event Interface <arch-app-event>`. ``esp_ble_audio_common_start`` takes an ``esp_ble_audio_start_info_t`` carrying the coordinated-set (CSIS) service instances to start. What the two calls do internally is the subject of :ref:`Initialization Flow <arch-init-flow>`: a handshake with the prebuilt library -- the shared-structure ABI (Application Binary Interface) check and the Kconfig configuration push -- followed by the per-service registration.
- **Opaque, transport-aliased types.** The event types an application handles -- ``esp_ble_audio_gap_app_event_t`` and ``esp_ble_audio_gatt_app_event_t`` -- are typedefs of the transport's ``bt_le_gap_app_event`` / ``bt_le_gatt_app_event``, and the ``ESP_BLE_AUDIO_GAP_EVENT_*`` / ``ESP_BLE_AUDIO_GATT_EVENT_*`` codes alias the transport's, so the audio and ISO layers present one event model.
- **Error codes.** Every function returns ``esp_err_t``.
- **Per-role registration.** A profile role follows the uniform shape from :ref:`Clients and Controllers <arch-audio-profiles>`: enable the role's Kconfig option, register its callback structure through the role's ``<profile>_api.h``, and drive it through that header's functions. Registering a role is also what adds its GATT service to the table (see :ref:`deferred registration <arch-audio-deferred-add>`).
Functional Groups
~~~~~~~~~~~~~~~~~
.. list-table:: ESP-BLE-AUDIO public API by purpose
:header-rows: 1
:widths: 24 42 34
* - Group
- Representative functions
- Purpose
* - Initialization
- ``esp_ble_audio_common_init``
- Register the GAP and GATT callbacks and bring up the prebuilt library, GAP and GATT.
* - Start
- ``esp_ble_audio_common_start``
- Register the coordinated-set services (CSIS, CAS) from ``start_info`` and start the GATT server (the second BTA phase on Bluedroid).
* - GATT discovery
- ``esp_ble_audio_gattc_disc_start``
- Start GATT discovery of a peer's services on a connection.
* - Event forwarding
- ``esp_ble_audio_gap_app_post_event``,\ |br|\ ``esp_ble_audio_gatt_app_post_event``
- Forward host GAP and GATT events into the engine (NimBLE only -- see below).
* - LTV helpers
- ``esp_ble_audio_data_parse``,\ |br|\ ``esp_ble_audio_data_get_val``
- Parse the length-type-value metadata Bluetooth LE Audio uses throughout.
* - Per-profile roles
- the ``<profile>_api.h`` register and operation functions
- Take and drive a profile role -- the bulk of the API (see :ref:`Clients and Controllers <arch-audio-profiles>`).
Per-Host Integration Differences
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As with ESP-BLE-ISO, the only host-divergent entry points are the event-forwarding functions, and they exist for the same reason -- letting the engine see the GAP and GATT events for a connection the application drives:
- ``esp_ble_audio_gap_app_post_event`` and ``esp_ble_audio_gatt_app_post_event`` are **needed only on NimBLE**: the host delivers GAP and GATT events to the callbacks the application registered, and the application forwards them inward with these. On Bluedroid the adapter installs its own BTA/BTM callbacks and captures the events directly, so no forwarding is required -- ``esp_ble_audio_gatt_app_post_event`` is even hidden from Bluedroid builds, so calling it there is a compile-time error.
@@ -0,0 +1,284 @@
.. _ble-audio-architecture:
ESP-IDF Bluetooth LE Audio Architecture
=======================================
:link_to_translation:`zh_CN:[中文]`
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 :ref:`ESP-BLE-ISO Public API <arch-iso-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 (:ref:`ESP-BLE-ISO <arch-iso-api>`, :ref:`ESP-BLE-AUDIO <arch-audio-api>`). 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.
.. |br| raw:: html
<br/>
.. contents:: Table of Contents
:local:
:depth: 1
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.
.. mermaid::
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
~~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 16 40 44
* - 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``
.. _arch-dual-host:
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 :ref:`Callback Execution Context <arch-callback-context>`.
.. list-table:: Host-level differences
:header-rows: 1
:widths: 26 37 37
* - 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:
.. list-table::
:header-rows: 1
:widths: 26 74
* - Page
- What it covers
* - :ref:`ESP-BLE-ISO <arch-iso-transport>`
- - 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
* - :ref:`ESP-BLE-AUDIO <arch-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
.. toctree::
:hidden:
:maxdepth: 1
ble-audio-architecture-iso
ble-audio-architecture-lea
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:
.. list-table:: Intentional per-host differences
:header-rows: 1
:widths: 30 35 35
* - 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 :ref:`Concurrency and Thread Safety <arch-concurrency>` 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
~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 34 66
* - 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``,\ |br|\ ``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
~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 34 66
* - 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``,\ |br|\ ``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.
@@ -0,0 +1,83 @@
Feature Support Status
======================
:link_to_translation:`zh_CN:[中文]`
This page tracks the support status of Bluetooth LE Audio features in ESP-IDF — the Generic Audio Framework profiles and services provided by ESP-BLE-AUDIO, and the isochronous transport provided by ESP-BLE-ISO.
.. note::
The Bluetooth LE Audio features marked as supported below are currently a **preview**: their APIs and behavior are provisional and may change in future releases.
The table below lists the Bluetooth LE Audio profiles and services currently supported in ESP-IDF.
.. list-table::
:header-rows: 1
:widths: 28 12 60
* - Profile / Service
- Supported
- Notes
* - LE Isochronous Channels (CIS / BIS)
- Yes
- Direct ISO access via :doc:`ESP-BLE-ISO <../../api-reference/bluetooth/esp-ble-iso>`.
* - BAP
- Yes
- All six BAP roles: Unicast Client, Unicast Server, Broadcast Source, Broadcast Sink, Broadcast Assistant, Scan Delegator.
* - PACS
- Yes
- Used by BAP Unicast Server and Broadcast Sink.
* - ASCS
- Yes
- Used by BAP Unicast Server.
* - BASS
- Yes
- Used by BAP Scan Delegator and Broadcast Assistant.
* - CAP
- Yes
- All three CAP roles: Acceptor, Initiator, Commander.
* - CAS
- Yes
- Mandatory service on CAP Acceptors.
* - CSIP / CSIS
- Yes
- Set Member and Set Coordinator roles.
* - VCP / VCS
- Yes
- Volume Renderer and Volume Controller roles.
* - VOCS
- Yes
- Per-output volume offset control; included in VCS as an optional sub-service.
* - AICS
- Yes
- Audio input control; included in VCS and MICS as an optional sub-service.
* - MICP / MICS
- Yes
- Microphone Device and Microphone Controller roles.
* - MCP / MCS
- Partial
- Media Control Server and Media Control Client roles are supported. OTP/OTS-based media object transfer is not currently supported.
* - CCP / TBS
- Yes
- Call Control Server and Call Control Client roles, including GTBS and TBS.
* - HAP / HAS
- Yes
- Hearing Aid and Hearing Aid Unicast Client roles, including preset read/write via HAS.
* - TMAP / TMAS
- Yes
- All six TMAP roles: CG, CT, UMS, UMR, BMS, BMR.
* - GMAP / GMAS
- Yes
- All four GMAP roles: UGG, UGT, BGS, BGR.
* - PBP
- Yes
- Public Broadcast Source and Public Broadcast Sink roles.
* - OTP / OTS
- No
- Object Transfer Profile/Service (used by MCP/MCS for media object transfer) is not currently supported.
.. note::
For the profile and service definitions, see :doc:`Bluetooth LE Audio Standard <ble-audio-introduction>`.
For general Bluetooth Low Energy feature support, see :doc:`Major Feature Support Status <../ble/ble-feature-support-status>`.
@@ -0,0 +1,25 @@
ESP-IDF Bluetooth LE Audio
==========================
:link_to_translation:`zh_CN:[中文]`
Bluetooth LE Audio is the audio architecture introduced in Bluetooth Core Specification 5.2. When reading this documentation, it helps to keep two ideas separate:
- The **standard** is the set of profiles, services, and roles defined by the Bluetooth SIG — the Generic Audio Framework, layered on top of the LE isochronous transport. It is platform-independent and is described in :doc:`Bluetooth LE Audio Standard <ble-audio-introduction>`.
- The **implementation** is the software that realizes the standard on a particular platform. The ESP-IDF implementation is described in :doc:`ESP-IDF Bluetooth LE Audio Architecture <ble-audio-architecture-overview>`.
In ESP-IDF, the implementation is delivered as two components:
- **ESP-BLE-ISO** provides the isochronous transport — the Connected and Broadcast Isochronous Streams (CIS/BIS) that carry the audio. See its :doc:`API reference <../../api-reference/bluetooth/esp-ble-iso>`.
- **ESP-BLE-AUDIO** provides the Generic Audio Framework profiles and services, built on top of ESP-BLE-ISO. See its :doc:`API reference <../../api-reference/bluetooth/esp-ble-audio>`.
Both components run on either of the two ESP-IDF Bluetooth host stacks, **Bluedroid** or **NimBLE**. Their API and behavior are kept identical across the two hosts wherever possible; where a behavioral difference is unavoidable, it is called out in the architecture document together with the reason.
For the support status of individual features, see :doc:`Feature Support Status <ble-audio-feature-support-status>`.
.. toctree::
:maxdepth: 1
ble-audio-introduction
ble-audio-architecture-overview
ble-audio-feature-support-status
@@ -1,57 +1,45 @@
LE Audio Architecture Guide
============================
Bluetooth LE Audio Standard
===========================
:link_to_translation:`zh_CN:[中文]`
This document introduces the Bluetooth LE Audio architecture: its profiles, services, the roles they define, and the dependency relationships among them. It is intended as a conceptual reference to help you choose the right set of profiles for your application before working with the :doc:`ESP-BLE-AUDIO API reference <../../api-reference/bluetooth/esp-ble-audio>`.
This document introduces the Bluetooth LE Audio specification: its profiles, services, the roles they define, and the dependency relationships among them. It is intended as a conceptual reference to help you choose the right set of profiles for your application before working with the :doc:`ESP-BLE-AUDIO API reference <../../api-reference/bluetooth/esp-ble-audio>`.
.. note::
This document covers the Bluetooth LE Audio **specification** — its profiles, services, roles, and dependencies. For the **ESP-IDF implementation** architecture (the ESP-BLE-ISO and ESP-BLE-AUDIO components, their task and locking model, and the per-host adapters), see :doc:`ESP-IDF Bluetooth LE Audio Architecture <ble-audio-architecture-overview>`.
Overview
--------
Bluetooth LE Audio is a suite of specifications introduced in Bluetooth Core Specification 5.2. It enables high-quality, low-power audio over Bluetooth LE using the following key additions to the standard:
Bluetooth LE Audio, introduced in the Bluetooth Core Specification 5.2, enables high-quality, low-power audio over Bluetooth LE using the following key additions to the standard:
- **LE Isochronous Channels (ISO)** — A new controller-level transport for time-synchronized, low-latency data streams, supporting both connected (CIS) and connectionless (BIS) modes.
- **LC3 Codec** — The Low Complexity Communication Codec, which provides better audio quality at lower bitrates compared to SBC.
- **Generic Audio Framework (GAF)** — A layered set of profiles and services that standardize audio stream setup, volume control, media control, call control, and device coordination.
LE Audio supports two fundamental audio scenarios:
Bluetooth LE Audio supports two fundamental audio scenarios:
- **Unicast Audio** — Bidirectional or unidirectional audio between two connected devices over Connected Isochronous Streams (CIS). Typical use cases: TWS earbuds, hearing aids, headsets, telephony.
- **Broadcast Audio (Auracast™)** — Unidirectional audio from one broadcaster to any number of receivers over Broadcast Isochronous Streams (BIS). Typical use cases: public venue audio, accessibility assistive listening, group TV listening.
Architecture Overview
---------------------
Specification Overview
----------------------
The LE Audio architecture is organized into three tiers:
The Bluetooth LE Audio specification is organized into three tiers:
1. **LE Isochronous Channels** — The transport layer, providing CIS (connected) and BIS (broadcast) streams.
1. **Transport** — The underlying Bluetooth LE transport: LE Isochronous Channels (CIS/BIS) carry the audio data, the ACL connection carries GATT/ATT profile control, and periodic advertising carries broadcast announcements. Only the isochronous channels are new to LE Audio (detailed below).
2. **Generic Audio Framework (GAF)** — The core specification suite, organized into four functional layers: stream control, content control, rendering/capture control, and transition/coordination control.
3. **Use-Case Specific Profiles** — Higher-level profiles (HAP, TMAP, GMAP, PBP) that select and configure specific GAF components for a target use case.
.. code-block:: none
.. figure:: ../../../_static/ble/ble-audio-gaf-en.png
:align: center
:width: 90%
:alt: Bluetooth LE Audio specification stack
┌──────────────────────────────────────────────────────────────────────┐
│ Use-Case Specific Profiles │
│ HAP TMAP GMAP PBP │
├──────────────────────────────────────────────────────────────────────┤
│ Generic Audio Framework (GAF) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Transition & CAP + CAS │ │
│ │ Coordination Control CSIP + CSIS │ │
│ ├───────────────────────────────────────────────────────────────┤ │
│ │ Rendering & VCP (VCS, VOCS, AICS) │ │
│ │ Capture Control MICP (MICS, AICS) │ │
│ ├───────────────────────────────────────────────────────────────┤ │
│ │ Content Control MCP + MCS/GMCS │ │
│ │ CCP + TBS/GTBS │ │
│ ├───────────────────────────────────────────────────────────────┤ │
│ │ Stream Control BAP (PACS, ASCS, BASS) │ │
│ └───────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ LE Isochronous Channels (CIS / BIS) │
└──────────────────────────────────────────────────────────────────────┘
The Bluetooth LE Audio stack: use-case profiles build on the Generic Audio Framework (GAF). Profile control rides GATT/ATT over the ACL connection, audio data rides the LE isochronous channels (CIS/BIS), and broadcast announcements ride periodic advertising.
Each GAF layer depends only on the layers below it. Use-case profiles select a subset of GAF layers and add role-specific constraints on top. The sections below describe each component in detail.
@@ -59,7 +47,7 @@ Each GAF layer depends only on the layers below it. Use-case profiles select a s
LE Isochronous Channels
-----------------------
LE Isochronous Channels are a feature of the Bluetooth controller, defined in the Bluetooth Core Specification. They provide the time-synchronized, low-latency data transport that LE Audio relies on.
LE Isochronous Channels are a feature of the Bluetooth controller, defined in the Bluetooth Core Specification. They provide the time-synchronized, low-latency data transport that Bluetooth LE Audio relies on.
.. list-table::
:header-rows: 1
@@ -75,13 +63,13 @@ LE Isochronous Channels are a feature of the Bluetooth controller, defined in th
- BIS
- Unidirectional isochronous stream from a Broadcaster to any number of Synchronized Receivers, without a prior connection. Multiple BIS instances belong to a Broadcast Isochronous Group (BIG).
ESP-IDF provides direct access to CIS and BIS via the :doc:`ESP-BLE-ISO API <../../api-reference/bluetooth/esp-ble-iso>`. When using LE Audio profiles (BAP and above), the ISO layer is managed automatically by the profile stack.
ESP-IDF provides direct access to CIS and BIS via the :doc:`ESP-BLE-ISO API <../../api-reference/bluetooth/esp-ble-iso>`. When using Bluetooth LE Audio profiles (BAP and above), the ISO layer is managed automatically by the profile stack.
Generic Audio Framework
-----------------------
The Generic Audio Framework (GAF) is the core of LE Audio. It defines four functional layers, described below from the bottom up.
The Generic Audio Framework (GAF) is the core of Bluetooth LE Audio. It defines four functional layers, described below from the bottom up.
Stream Control Layer
@@ -91,7 +79,7 @@ The stream control layer is responsible for discovering audio capabilities, sett
**Basic Audio Profile (BAP)**
BAP is the foundational profile for all LE Audio streaming. It defines the following roles:
BAP is the foundational profile for all Bluetooth LE Audio streaming. It defines the following roles:
- **Unicast Client** — Discovers ASEs on a remote Unicast Server, initiates codec configuration, QoS negotiation, and stream control (enable, connect, start, disable, release).
- **Unicast Server** — Exposes audio endpoints (ASEs) via ASCS and responds to client-initiated stream control procedures.
@@ -301,10 +289,72 @@ PBP standardizes the metadata format used by a public broadcast source so that a
PBP depends entirely on BAP for the underlying broadcast transport; it does not define a new GATT service.
Profile and Service Dependency Reference
----------------------------------------
Profile and Service Dependencies
--------------------------------
The diagram below expands each abbreviation to its full name and shows the layered dependency hierarchy — solid arrows are a depends-on relationship, dotted arrows an optional included sub-service. The tables that follow give the exact per-profile dependencies.
.. mermaid::
%%{init: {'flowchart': {'nodeSpacing': 35, 'rankSpacing': 65}}}%%
flowchart LR
HAP["HAP<br/>(Hearing Access Profile)"]
TMAP["TMAP<br/>(Telephony and Media Audio Profile)"]
GMAP["GMAP<br/>(Gaming Audio Profile)"]
PBP["PBP<br/>(Public Broadcast Profile)"]
CAP["CAP<br/>(Common Audio Profile)"]
VCP["VCP<br/>(Volume Control Profile)"]
MICP["MICP<br/>(Microphone Control Profile)"]
CSIP["CSIP<br/>(Coordinated Set Identification Profile)"]
MCP["MCP<br/>(Media Control Profile)"]
CCP["CCP<br/>(Call Control Profile)"]
BAP["BAP<br/>(Basic Audio Profile)"]
HAS["HAS<br/>(Hearing Access Service)"]
TMAS["TMAS<br/>(Telephony and Media Audio Service)"]
GMAS["GMAS<br/>(Gaming Audio Service)"]
CAS["CAS<br/>(Common Audio Service)"]
PACS["PACS<br/>(Published Audio Capabilities Service)"]
ASCS["ASCS<br/>(Audio Stream Control Service)"]
BASS["BASS<br/>(Broadcast Audio Scan Service)"]
CSIS["CSIS<br/>(Coordinated Set Identification Service)"]
VCS["VCS<br/>(Volume Control Service)"]
MICS["MICS<br/>(Microphone Control Service)"]
MCS["MCS / GMCS<br/>(Media Control Service)"]
TBS["TBS / GTBS<br/>(Telephone Bearer Service)"]
VOCS["VOCS<br/>(Volume Offset Control Service)"]
AICS["AICS<br/>(Audio Input Control Service)"]
OTS["OTS<br/>(Object Transfer Service)"]
HAP --> CAP
TMAP --> CAP & MCP & CCP
GMAP --> CAP
PBP --> BAP
CAP --> BAP & VCP & MICP & CSIP
CAP --> CAS
HAP --> HAS
TMAP --> TMAS
GMAP --> GMAS
BAP --> PACS & ASCS & BASS
VCP --> VCS
MICP --> MICS
CSIP --> CSIS
MCP --> MCS
CCP --> TBS
VCS -.-> VOCS & AICS
MICS -.-> AICS
MCS -.-> OTS
CAP ~~~ MCP
CAP ~~~ CCP
classDef uc fill:#dbe8ff,stroke:#5b8def,color:#173;
classDef coord fill:#ffe6c7,stroke:#e0922f;
classDef ctrl fill:#e3f6da,stroke:#5aa84f;
classDef svc fill:#efe3ff,stroke:#9a6fd6;
classDef opt fill:#f0f0f0,stroke:#9e9e9e,color:#555;
class HAP,TMAP,GMAP,PBP uc;
class CAP,BAP coord;
class VCP,MICP,CSIP,MCP,CCP ctrl;
class HAS,TMAS,GMAS,CAS,PACS,ASCS,BASS,CSIS,VCS,MICS,MCS,TBS svc;
class VOCS,AICS,OTS opt;
The following tables summarize the dependencies between profiles and services in the ESP-IDF LE Audio implementation.
Profile-to-Service Dependencies
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -414,84 +464,3 @@ The table below maps common product types to the profiles they require.
- GMAP UGT (receiver), BAP Unicast Server, VCP Volume Renderer
* - Media Sender (audio bar)
- TMAP UMS (or BMS for broadcast), BAP, MCP/MCS server, VCP
ESP-IDF Implementation
-----------------------
ESP-IDF provides two API components for LE Audio:
- :doc:`ESP-BLE-ISO <../../api-reference/bluetooth/esp-ble-iso>` — Direct access to LE Isochronous Channels (CIS/BIS) for applications that manage their own ISO data paths.
- :doc:`ESP-BLE-AUDIO <../../api-reference/bluetooth/esp-ble-audio>` — High-level LE Audio profile and service APIs covering the full GAF stack (BAP, CAP, VCP, MICP, CSIP, MCP, CCP, HAP, GMAP, TMAP, PBP) and codec support (LC3).
For most applications, ESP-BLE-AUDIO is the correct starting point. ESP-BLE-ISO is intended for advanced use cases that require direct ISO control below the profile layer.
Feature Support
^^^^^^^^^^^^^^^
The table below lists the LE Audio profiles and services currently supported in ESP-IDF.
.. list-table::
:header-rows: 1
:widths: 28 12 60
* - Profile / Service
- Supported
- Notes
* - LE Isochronous Channels (CIS / BIS)
- Yes
- Direct ISO access via :doc:`ESP-BLE-ISO <../../api-reference/bluetooth/esp-ble-iso>`.
* - BAP
- Yes
- All six BAP roles: Unicast Client, Unicast Server, Broadcast Source, Broadcast Sink, Broadcast Assistant, Scan Delegator.
* - PACS
- Yes
- Used by BAP Unicast Server and Broadcast Sink.
* - ASCS
- Yes
- Used by BAP Unicast Server.
* - BASS
- Yes
- Used by BAP Scan Delegator and Broadcast Assistant.
* - CAP
- Yes
- All three CAP roles: Acceptor, Initiator, Commander.
* - CAS
- Yes
- Mandatory service on CAP Acceptors.
* - CSIP / CSIS
- Yes
- Set Member and Set Coordinator roles.
* - VCP / VCS
- Yes
- Volume Renderer and Volume Controller roles.
* - VOCS
- Yes
- Per-output volume offset control; included in VCS as an optional sub-service.
* - AICS
- Yes
- Audio input control; included in VCS and MICS as an optional sub-service.
* - MICP / MICS
- Yes
- Microphone Device and Microphone Controller roles.
* - MCP / MCS
- Partial
- Media Control Server and Media Control Client roles are supported. OTP/OTS-based media object transfer is not currently supported.
* - CCP / TBS
- Yes
- Call Control Server and Call Control Client roles, including GTBS and per-bearer TBS.
* - HAP / HAS
- Yes
- Hearing Aid and Hearing Aid Unicast Client roles, including preset read/write via HAS.
* - TMAP / TMAS
- Yes
- All six TMAP roles: CG, CT, UMS, UMR, BMS, BMR.
* - GMAP / GMAS
- Yes
- All four GMAP roles: UGG, UGT, BGS, BGR.
* - PBP
- Yes
- Public Broadcast Source and Public Broadcast Sink roles.
* - OTP / OTS
- No
- Object Transfer Profile/Service (used by MCP/MCS for media object transfer) is not currently supported.
+3
View File
@@ -129,3 +129,6 @@ get-started/windows-setup-update get-started/windows-setup
# Kconfig documentation got pretty serious file structure changes
api-reference/kconfig api-reference/kconfig-reference
# The LE Audio overview page 'ble-audio' is now the 'esp-ble-audio' landing page
api-guides/ble/ble-audio api-guides/esp-ble-audio/ble-audio-index
+1 -1
View File
@@ -46,4 +46,4 @@
:SOC_BLE_MESH_SUPPORTED: ../esp-ble-mesh/ble-mesh-index
:SOC_BLUFI_SUPPORTED: blufi
:SOC_BLE_AUDIO_SUPPORTED: ble-audio
:SOC_BLE_AUDIO_SUPPORTED: ../esp-ble-audio/ble-audio-index
@@ -0,0 +1,585 @@
.. _arch-iso-transport:
ESP-BLE-ISO
===========
:link_to_translation:`en:[English]`
.. |br| raw:: html
<br/>
.. contents:: 目录
:local:
:depth: 2
ESP-BLE-ISO 提供了上层规范所需的每一个 BLE 传输原语:ACL 连接状态、广播与扫描、周期性广播同步、HCI 命令路径、ISO(CIS 和 BIS)、GATT、GAP 和 L2CAP,以及 :ref:`并发与线程安全 <arch-concurrency>` 中描述的 ISO 任务事件循环和全局锁。这些都不是音频专用的;ESP-IDF 蓝牙 LE Audio 只是目前位于其上的规范层。本章依次介绍整个组件:连接管理、广播与扫描、HCI 命令路径、ISO 子系统、GAP、GATT 和 L2CAP,以及公共 API。
整个组件中反复出现两个约定,值得在此一次性说明:
- ``_safe`` **后缀**。大多数内部操作都成对存在:一个核心函数假定全局 ISO 锁已被持有,以及一个同名、带 ``_safe`` 后缀的包装函数,它获取锁、调用核心函数、再释放锁。公共入口点以及来自 ISO 任务之外的调用者使用 ``_safe`` 变体;已经在锁下运行的代码(ISO 任务处理函数,或另一个核心函数)直接调用裸变体。这就是 :ref:`保护共享状态 <arch-host-lock>` 中那把唯一的全局锁如何被统一应用、而不会意外重入的方式 —— 尽管该互斥锁是递归的,这种拆分仍让每个调用点处的锁边界保持明确。
- ``bt_le_*`` / ``bt_*`` **命名**。以 ``bt_`` 开头的符号是 ``host/common`` 中定义的与主机无关的接口;以 ``bt_le_bluedroid_`` 或 ``bt_le_nimble_`` 开头的符号是仅由一个构建编译的适配器实现。
- 有一族是例外:``host/common`` 中的 ``hci_le_*`` ISO 事件处理函数(例如 ``hci_le_biginfo_adv_report``)是以它们所解码的 HCI 事件命名的,而非以 ``bt_`` 接口命名,并且它们是内部处理函数,而非公共接口的一部分。
.. _arch-concurrency:
并发与线程安全
------------------------
主机协议栈之上的所有 ESP-IDF 蓝牙 LE Audio 处理都运行在单个任务上 —— **ISO 任务**\ (``iso_task``)—— 并且对所有共享状态的访问都由一把全局递归互斥锁串行化,即 **ISO 锁**\ (``bt_le_host_lock``)。这两种机制共同保证了协议栈的线程安全;理解它们是厘清顺序与竞态问题的关键。
ISO 任务事件循环
~~~~~~~~~~~~~~~~~~~~~~~~
ISO 任务(位于 ``host/common/task.c``)是一个永远循环的单个 FreeRTOS 任务,负责排空协议栈其余部分通过 ``bt_le_iso_task_post()`` 投递给它的工作。每一项携带一个事件类型标签,任务据此将其分发给对应类别的处理函数 —— 定时器、GAP、GATT、ISO HCI、ISO 发送完成或 ISO 接收数据。
投递的工作并非单一队列,而是\ **三个优先级层**\ ,每层是一条独立的 FreeRTOS 队列,合并到任务所阻塞等待的一个队列集(queue set)中。每次被唤醒时,任务都\ **严格按优先级**\ 处理各层 —— 先 critical、再 normal、最后 floodable —— 每轮循环处理一项,并在下一轮重新优先检查 critical 层:
.. list-table:: ISO 任务优先级层
:header-rows: 1
:widths: 16 12 22 50
* - 层
- 深度
- 溢出时
- 事件
* - **Critical**
- 32
- 丢弃最新项
- ISO 接收数据与发送完成 —— 延迟敏感的数据通路,由控制器任务以非阻塞方式投递。
* - **Normal**
- 64
- 阻塞(绝不丢弃)
- 定时器、GAP 生命周期、GATT 和 ISO HCI 事件 —— 可靠;生产者阻塞直到有空位。
* - **Floodable**
- 32
- 丢弃最新项
- 高频、尽力而为的 GAP 报告 —— 扩展广播、周期性广播和 BIGInfo 报告 —— 以非阻塞方式投递。
这种拆分的目的是让 GAP 报告的突发洪峰无法拖延 ISO 数据:critical 层总是最先被排空,而两个非阻塞层在满时丢弃其最新项,而不是阻塞其生产者。事件落入哪一层与运行它的处理函数无关 —— floodable 层上的扩展广播和周期性广播报告,由与 normal 层 GAP 事件相同的 ``bt_le_gap_handle_event`` 分发;floodable 层的 BIGInfo 报告,由与普通 ISO HCI 事件相同的 ``bt_le_iso_handle_hci_event`` 分发。
.. note::
单个任务排空全部三层,因此事件仍然\ **一次一个地处理、永不重叠**\ ,无论由哪个任务产生 —— 协议栈其余部分所依赖的串行化保证保持不变。各层改变的是\ **顺序**\ :事件按优先级顺序分发,而非按投递顺序,因此一个高优先级的 ISO 数据事件可以先于更早投递的 GAP 报告被处理。依赖串行化保证的代码仍然正确;假定跨事件类别严格先入先出顺序的代码则不然。
该任务有几个值得记住的特性:
- 它运行在 **4 KB 栈**\ 上。每个上层(包括应用和规范回调)都在这个栈上执行,因此必须避免深层调用链和大的栈上分配缓冲区(例如根据配置值确定大小的数组)。
- 每个入队项携带一个事件类型标签和一个堆上分配的负载;由生产者分配负载,处理函数在处理完后释放它 —— 但在某个非阻塞层已满时被丢弃的项例外,它由生产者释放。
- 它的 CPU 核和优先级与当前激活的主机协议栈保持一致,如 :ref:`双主机设计 <arch-dual-host>` 中所述。
一个可选的\ **分发监视器**\ (``CONFIG_BT_ISO_DISPATCH_MONITOR``,默认关闭)会为任务分发的每个回调计时,并保留按事件类型的统计 —— 计数、最大时长,以及超过 ``CONFIG_BT_ISO_DISPATCH_THRESHOLD_US``\ (默认 2000 微秒,约为 7.5–10 毫秒 SDU 间隔的四分之一)的回调的慢计数。该统计表会周期性(``CONFIG_BT_ISO_DISPATCH_DUMP_PERIOD_S``,默认 10 秒)以及在反初始化时打印,用于发现运行时间长到足以拖延 ISO 数据通路的回调。它会增加每次分发的计时开销并测量挂钟时间,因此仅用于性能分析。
.. _arch-host-lock:
保护共享状态
------------------------
共享状态 —— 连接表、周期性广播同步表、GATT 订阅列表、GATT 服务端配置、ISO 簿记 —— 可以从多个任务访问。协议栈\ **不**\ 使用细粒度的、按结构划分的锁。相反,线程安全依赖于一把粗粒度的\ **全局递归互斥锁** —— 即 **ISO 锁**,``bt_le_host_lock`` / ``bt_le_host_unlock``\ (在 ``host/common/host.c`` 中基于 Zephyr 的 ``k_mutex`` 实现)。
规则简单且处处适用:**任何触碰共享状态的代码路径都必须先获取这把互斥锁。**\ 实践中这意味着:
- **公共 API 函数**\ ,从应用任务调用,在进入时获取锁,在返回前释放。
- **ISO 任务事件处理函数**\ ,在每个处理函数中读取或修改共享状态的部分前后获取锁。
- **NimBLE 同步回调**\ (订阅变更、收到的通知、GATT 服务端属性访问路径),在触碰共享状态前获取锁。
- **定时器回调**\ 同样获取锁。
由于某个变量的每一个读者和写者都持有同一把锁,任何两个任务都不可能并发触碰它。这把互斥锁是\ **递归的**\ ,因此已经持有锁的处理函数可以调用会重新获取它的辅助函数而不会死锁。
如果在一个短的有限时间内无法获取锁,``bt_le_host_lock`` 会调用 ``abort()`` 而非继续执行。锁被持有这么久意味着协议栈已经卡死(死锁或卡住的回调),这是编程错误,而非需要在运行时恢复的状况。这里特意使用 ``abort()`` 而非 ``assert()``,因为 ``assert()`` 在 ``NDEBUG`` 构建中会变成空操作,会让调用者在未持有锁的情况下进入临界区,从而重新引入竞态。解锁路径还会额外验证调用任务是否为当前持有者,从而捕获不配对的解锁。
.. important::
这里有两种不同的串行化机制在起作用,它们覆盖不同的路径:
#. **三条优先级队列背后的单个消费者**\ 串行化所有投递到 ISO 任务的事件(源自主机协议栈的 GAP、GATT、ISO 和定时器事件)。已投递的事件一次一个地处理、永不重叠,不过跨层时它们按优先级顺序而非投递顺序分发。
#. **全局 ISO 锁**\ 串行化其他上下文 —— 直接的应用 API 调用和 NimBLE 同步回调 —— 使其与 ISO 任务处理函数互斥。
一份假设两个已投递事件并发运行的缺陷报告几乎总是误报:单消费者从根本上排除了这种情况。
.. _arch-callback-context:
回调执行上下文
------------------------
应用注册的回调(GAP 事件处理函数、GATT 属性处理函数、规范事件回调)并不总是运行在同一个任务上,而且在一个重要场景下,这个任务\ **在两种主机之间不同**\ 。了解上下文很重要:运行在 ISO 任务上的回调共享其 4 KB 栈,绝不能阻塞它;而运行在主机任务上的回调,则在主机的属性访问路径内部同步执行。
.. list-table:: 已注册回调运行在哪个任务上
:header-rows: 1
:widths: 44 28 28
* - 回调类别
- Bluedroid
- NimBLE
* - GAP 事件(连接、断开、安全变更、PA 同步)
- ISO 任务
- ISO 任务
* - GATT 客户端通知与发现结果
- ISO 任务
- ISO 任务
* - GATT 客户端读写完成
- ISO 任务
- **NimBLE 主机任务**
* - ISO 事件(连接、数据、发送完成)
- ISO 任务
- ISO 任务
* - 定时器回调
- ISO 任务
- ISO 任务
* - GATT 服务端属性读/写与 CCC 订阅,以及任何由入站写同步驱动的规范回调
- ISO 任务
- **NimBLE 主机任务**
有两行将回调放在 NimBLE 主机任务上,而非 ISO 任务。GATT 客户端读写完成落在那里,是因为 NimBLE 通过其主机任务上的完成回调报告流程结果,适配器在 ISO 锁下原地(inline)调用应用回调,而不是重新投递它 —— 相比之下,Bluedroid 会把每个 BTA GATT 事件都投递到 ISO 任务。GATT 服务端属性访问那一行有更鲜明的原因:两种主机以相反的方式完成访问。在 Bluedroid 上,请求被投递到 ISO 任务,已注册的回调在那里运行,而响应随后\ **异步**\ 发送,如下图所示:
.. mermaid::
sequenceDiagram
participant P as Peer
participant BTU as BTU task
participant T as ISO task
participant CB as attr read/write callback
P->>BTU: ATT 读 / 写请求
BTU->>T: 投递 GATTS 事件
T->>CB: 调用回调(在 ISO 锁下)
CB-->>T: 值 / 状态
T->>BTU: BTA_GATTS_SendRsp(异步)
BTU-->>P: ATT 响应
在 NimBLE 上则相反,主机要求\ **同步**\ 返回该值,因此回调在主机任务上原地运行,并直接把值交回:
.. mermaid::
sequenceDiagram
participant P as Peer
participant N as NimBLE host task
participant CB as attr read/write callback
P->>N: ATT 读 / 写请求
N->>CB: 原地调用回调(在 ISO 锁下)
CB-->>N: 值 / 状态
N-->>P: 同步发送响应
.. _arch-event-flow:
跨层事件流
------------------------
下图追踪一个典型的入站事件 —— 由控制器引发的 GAP、GATT 客户端或 ISO 事件 —— 从控制器一路向上到应用回调,并展示两种主机的差异之处。
.. mermaid::
%%{init: {'sequence': {'noteAlign': 'left'}}}%%
sequenceDiagram
participant C as BLE controller
participant H as Host stack task
participant A as ESP-BLE-ISO adapter
participant T as ISO task
participant CB as App/Profile callbacks
C->>H: HCI 事件
Note over H: Bluedroid:在 HCI 主机任务上接收,回调运行在 BTU<br/>NimBLE:单个主机任务两者都做
H->>A: 主机回调
Note over A: Bluedroid:BTA 回调<br/>NimBLE:*_cb_safe(获取 ISO 锁)
A->>A: 在堆上构建事件负载
A->>T: bt_le_iso_task_post() — 入队
T->>T: 出队,获取 ISO 锁
T->>CB: 在 ISO 任务上下文中调用回调
Note over C,T: ISO 数据 / 发送完成是例外 —— 控制器直接调用已注册的回调,绕过主机协议栈
C-->>T: bt_le_iso_task_post() — 入队(直接回调,在控制器任务上)
T->>T: 出队,获取 ISO 锁
T->>CB: 在 ISO 任务上下文中的通道回调
应用和规范回调运行在何处由这个流程决定,并且因主机而异:
- 在 **Bluedroid** 上,\ **每个上层回调都在 ISO 任务上下文中运行**\ ,无论由什么触发 —— GAP、GATT 客户端(通知、发现、读或写完成)、GATT 服务端、ISO 或定时器。适配器把每个 BTA 事件都投递到队列,因此没有例外。这些回调在持有 ISO 锁时运行于任务的 4 KB 栈上,从其中调用阻塞式传输 API 会拖住整个事件循环。
- 在 **NimBLE** 上,情况相同,但有两个类别例外,它们\ **原地运行在 NimBLE 主机任务上**\ 而非 ISO 任务:**GATT 服务端属性访问**\ (值必须同步返回)和 **GATT 客户端读写完成**\ (NimBLE 通过主机任务上的完成回调报告结果,适配器直接调用它)。因此,由二者之一驱动的规范回调 —— 例如服务端的控制点写处理函数,或客户端的读或写完成 —— 运行在主机任务上,而非 ISO 任务,且不经过队列。这正是 :ref:`回调执行上下文 <arch-callback-context>` 中标记的那两行。
分层与文件映射
------------------------
该组件被组织为:一个与主机无关的 ``host/common`` 层、两个 ``host/adapter`` 层、``host/iso`` 下的一个独立 ISO 引擎,以及 ``host/utils`` 下的共享辅助代码。与主机无关的文件如下:
.. list-table:: ``host/common`` —— 与主机无关的传输
:header-rows: 1
:widths: 24 76
* - 文件
- 作用
* - ``host.c``
- 初始化与反初始化编排;全局递归锁(``bt_le_host_lock`` / ``bt_le_host_unlock``)。
* - ``task.c``
- ISO 任务事件循环、其三条优先级队列和队列集,以及 ``bt_le_iso_task_post()``。
* - ``conn.c``
- ACL 连接表和连接事件监听器的分发。
* - ``adv.c``
- 扩展广播集的记录管理。
* - ``scan.c``
- 扫描、周期性广播同步,以及 BIGInfo 报告。
* - ``hci.c``
- 构建 HCI 命令缓冲区并分发给当前激活的适配器。
* - ``iso.c``
- 解码 ISO HCI 元事件,并把 ISO 数据通路桥接到引擎。
* - ``gatt.c``
- 与主机无关的 GATT 状态(订阅、属性数据库缓存)。
* - ``l2cap.c``
- 通用的 L2CAP 通道与服务端分发。
* - ``app/gap.c``
- 面向应用的 GAP 入口点。
* - ``app/gatt.c``
- 面向应用的 GATT 入口点。
ISO 状态机独立于通用层的其余部分,位于 ``host/iso/iso.c`` 中:它负责 CIG、BIG 和 ISO 通道逻辑,以及支撑它们的、按配置确定大小的静态池。它是 ``host/iso`` 下唯一的文件,在下文 :ref:`ISO 子系统 <arch-iso>` 中详述。
每个适配器目录都为其主机镜像了通用接口:``host/adapter/bluedroid`` 和 ``host/adapter/nimble`` 各自提供 ``gap.c``、``gatt``、``iso.c`` 和主机相关的头文件;``host/utils`` 则持有地址、UUID、CRC、加密、定时器和缓冲区等两层都无需重新实现的辅助代码。
.. note::
存在两个名为 ``iso.c`` 的文件,扮演不同角色。``host/iso/iso.c`` 是引擎 —— CIS 和 BIG 状态机以及公共 ISO 通道 API。``host/common/iso.c`` 是粘合层 —— 它把原始 HCI ISO 元事件解码到引擎的处理函数,并承载 ISO 数据的收发通路。适配器的 ``iso.c`` 文件把引擎命令翻译成主机原生调用。
连接管理
------------------------
``conn.c`` 负责 ACL 连接表 —— 一个以连接句柄为键的 ``bt_conn`` 对象数组 —— 以及连接事件监听器的注册列表。连接生命周期完全由主机适配器驱动:当控制器报告一个连接事件时,适配器调用某个监听器入口点,后者更新连接表,并把事件分发给上层通过 ``bt_conn_cb_register`` 注册的每一个 ``bt_conn_cb``。
这些监听器入口点构成了与主机无关的接缝:
- ``bt_le_acl_conn_connected_listener`` 和 ``bt_le_acl_conn_disconnected_listener`` 添加和移除表项。
- ``bt_le_acl_conn_security_changed_listener``、``..._identity_resolved_listener``、``..._pairing_completed_listener`` 和 ``..._bond_deleted_listener`` 承载安全与配对绑定事件。
由于适配器通过 ISO 任务投递这些事件(参见 :ref:`双主机设计 <arch-dual-host>`),已注册的 ``bt_conn_cb`` 回调在两种主机上都运行于 ISO 任务上下文 —— 即 :ref:`回调执行上下文 <arch-callback-context>` 中的 GAP 事件那一行。作用于活动连接的操作(``bt_conn_set_security``、``bt_conn_disconnect``、``bt_conn_get_info``)和查找辅助函数(``bt_conn_lookup_handle``、``bt_le_acl_conn_find``、``bt_conn_foreach``)都在 ISO 锁下从同一张表读取。这些事件的主机相关来源 —— 适配器调用监听器时所在的主机任务,以及它如何映射原生事件结构 —— 在下文 :ref:`应用事件接口 <arch-app-event>` 中介绍。
广播与扫描
------------------------
``adv.c`` 被刻意保持得很小:它按句柄跟踪扩展广播集(``bt_le_ext_adv_find`` / ``bt_le_ext_adv_new_safe`` / ``bt_le_ext_adv_delete_safe``)。广播集是广播 ISO 组所附着的锚点,因此这张表是 ISO 子系统中 BIG 广播端流程的前提。
``scan.c`` 覆盖三项相关职责:
- **扫描**。``bt_le_scan_cb`` 注册表;广播报告到达 ``bt_le_scan_recv_listener`` 并被递交给应用。
- **周期性广播同步**。同步表(``bt_le_per_adv_sync_new`` / ``..._delete`` / ``..._lookup_addr``)及其监听器 —— ``..._establish_listener``、``..._lost_listener`` 和 ``..._report_recv_listener`` —— 跟踪对广播源周期性广播序列的同步。
- **BIGInfo 报告**。``hci_le_biginfo_adv_report`` 呈现搭载在周期性广播序列上的 BIGInfo。BIGInfo 携带接收端同步到广播等时组所需的参数,因此该处理函数是从扫描进入 BIG 接收端流程的桥梁。
因此,周期性广播同步是接收广播音频的入口点:扫描、同步到周期性广播序列、读取 BIGInfo,然后请求 ISO 子系统同步到该 BIG。
HCI 命令路径
------------------------
ISO 引擎从不直接与主机协议栈对话。它用 ``bt_hci_cmd_create(opcode, len)`` 构建一个标准 HCI 命令缓冲区,并用 ``bt_hci_cmd_send_sync(opcode, buf, rsp)`` 提交它(二者都在 ``host/common/hci.c`` 中)。``bt_hci_cmd_send_sync`` 是一个分发到当前激活适配器 ISO 命令入口点的单行函数,而这正是两种主机急剧分化之处:
.. list-table:: 各主机的 HCI 命令转换
:header-rows: 1
:widths: 20 40 40
* - 方面
- Bluedroid
- NimBLE
* - 入口点
- ``bt_le_bluedroid_iso_cmd_send_sync``
- ``bt_le_nimble_iso_cmd_send_sync``
* - 转换
- 一个 opcode 分支;每个命令都通过一条私有的直连 HCI(direct-HCI)路径作为原始 HCI 命令转发。
- 一个 opcode 分支;每个命令都被拆解到 NimBLE 的类型化 ``ble_hs_hci_*`` ISO 辅助函数。
* - 同步
- ``adapter/bluedroid/hci.c`` 中的一个私有完成信号量;完成回调运行在 HCI 层任务上。
- 在 ``ble_hs_hci_*`` 调用内部处理。
Bluedroid 自带命令路径的原因是并发。Bluedroid 的 BTU 任务使用单个全局槽位来把一个同步命令与其完成相匹配;若从 ISO 任务经由同一个槽位下发 ISO 命令,会与 BTU 任务竞争。``adapter/bluedroid/hci.c`` 中的直连 HCI 路径完全绕开了这一点:每个命令携带自己的完成回调,调用者在一个专用信号量上等待,因此那个共享的 BTU 槽位从不被触碰。该回调运行在 HCI 层任务上并保持极简 —— 它把响应复制到一个静态接收区并发出信号量,不获取任何锁。NimBLE 不需要这些,因为它的 ``ble_hs_hci_*`` 辅助函数已经提供了一个自包含的同步命令接口。
.. mermaid::
sequenceDiagram
participant CORE as ISO engine (host/iso/iso.c)
participant HCI as bt_hci_cmd_send_sync (common/hci.c)
participant AD as Active adapter
participant CTRL as Controller
CORE->>HCI: 构建命令缓冲区(opcode + 参数)
HCI->>AD: *_iso_cmd_send_sync(opcode)
Note over AD: Bluedroid:direct-HCI(私有信号量)<br/>NimBLE:ble_hs_hci_* 类型化辅助函数
AD->>CTRL: HCI 命令
alt Command Complete(例如 Set CIG Parameters)
CTRL-->>AD: Command Complete
AD-->>CORE: 状态 + 返回参数
else Command Status(例如 Create CIS)
CTRL-->>AD: Command Status
AD-->>CORE: 状态(无返回参数)
Note over CORE,CTRL: 真正的结果稍后作为一个 LE 元事件到达<br/>(例如 CIS Established),<br/>在下文 ISO 子系统中处理
end
.. _arch-iso:
ISO 子系统
------------------------
ISO 子系统分布在三处:引擎(``host/iso/iso.c``)、元事件与数据粘合层(``host/common/iso.c``),以及适配器(``host/adapter/*/iso.c``)。引擎拥有三个按配置确定大小的静态池 —— 一个用于 ISO 通道(``CONFIG_BT_ISO_MAX_CHAN``)、一个用于连接等时组(``CONFIG_BT_ISO_MAX_CIG``)、一个用于广播等时组(``CONFIG_BT_ISO_MAX_BIG``)。每个公共引擎操作都遵循本节开头的 ``_safe`` 约定。
入站 ISO 元事件在两种主机上形态相同:适配器注册一个主机原生的 ISO 事件回调,封装该事件,并向 ISO 任务投递一个 ``ISO_HCI_EVENT`` 项;随后 ``host/common/iso.c`` 解码 LE 子事件并将其分发给引擎。一处线格式(wire-format)差异在此被吸收:NimBLE 的元事件结构已经包含子事件码,而 Bluedroid 适配器会在前面补上它,因此 ``host/common/iso.c`` 中的解码器无论在哪种主机上都看到统一的布局。
.. mermaid::
%%{init: {'sequence': {'noteAlign': 'left'}}}%%
sequenceDiagram
participant CTRL as Controller
participant AD as Active adapter
participant T as ISO task
participant CORE as ISO engine
participant APP as App callback
CTRL->>AD: ISO 元事件(HCI)
Note over AD: Bluedroid:BTM ISO 回调(BTU 任务)<br/>NimBLE:ISO 回调(主机任务)
AD->>T: 投递 ISO_HCI_EVENT
CTRL-->>T: ISO 数据,投递 ISO_RX_DATA
T->>CORE: 在 common/iso.c 中处理,然后引擎处理函数
CORE->>APP: 通道回调(在 ISO 锁下)
连接 ISO(CIS)
~~~~~~~~~~~~~~~~~~~~~~~~
连接等时流(CIS)是点对点的,并依托一条 ACL 连接。两种角色通过不同的入口点驱动引擎:
.. list-table:: CIS 角色
:header-rows: 1
:widths: 16 44 40
* - 角色
- 流程
- 关键符号
* - Central
- 配置一个组,然后向对端建立一条或多条流;控制器在每条流建立时予以确认。
- ``bt_iso_cig_create``,\ |br|\ ``bt_iso_cig_reconfigure``,\ |br|\ ``bt_iso_cig_terminate``,\ |br|\ ``bt_iso_chan_connect`` / ``hci_le_cis_established``
* - Peripheral
- 注册一个服务端,由它决定是否接受入站的流;随后每个对端请求被接受或拒绝,若接受则予以确认。
- ``bt_iso_server_register`` /\ |br|\ ``hci_le_cis_req`` / ``hci_le_cis_established``
广播 ISO(BIG)
~~~~~~~~~~~~~~~~~~~~~~~~
广播等时组(BIG)是无连接的,并依托一条周期性广播序列,而非 ACL 连接:
.. list-table:: BIG 角色
:header-rows: 1
:widths: 16 44 40
* - 角色
- 流程
- 关键符号
* - Broadcaster
- 把一个 BIG 附着到一个承载周期性序列的扩展广播集上;控制器确认该组,之后即可向各条流馈送数据。
- ``bt_iso_big_create`` / ``hci_le_big_complete``,\ |br|\ ``bt_iso_big_terminate`` / ``hci_le_big_terminate``
* - Receiver
- 同步到周期性序列,读取其 BIGInfo,然后同步到该组;同步丢失会被报告回来。
- ``bt_iso_big_sync`` / ``hci_le_big_sync_established``,\ |br|\ ``hci_le_big_sync_lost``
接收端流程始于 ``scan.c``:在已建立的周期性广播同步上由 ``hci_le_biginfo_adv_report`` 递交的 BIGInfo,正是 ``bt_iso_big_sync`` 用来加入该组的输入。
数据通路与数据流
~~~~~~~~~~~~~~~~~~~~~~~~
仅建立一条 CIS 或 BIS 还不足以传输音频;每条流都必须绑定到控制器的 ISO 数据通路。``bt_iso_setup_data_path`` 绑定一个方向(发送用输入,接收用输出),``bt_iso_remove_data_path`` 释放它。一条已连接但未建立数据通路的流不承载任何 SDU:建立流和绑定其数据通路是两个独立步骤,因此在某个上层显式建立通路之前,音频不会流动。
一旦数据通路建立,SDU 就经由 ``host/common/iso.c`` 流动:
- **发送**。应用用 ``bt_iso_chan_send``(或带时间戳的 ``bt_iso_chan_send_ts``)提交一个 SDU;引擎把它交给 ``bt_le_iso_tx``。如果控制器有空闲缓冲区且前面没有排队项,该 SDU 直接发往控制器;否则它被保留在主机侧的发送队列中。控制器的发送完成信号作为一个 ``ISO_TX_COMP`` 事件返回并投递到 ISO 任务,在那里 ``bt_le_iso_handle_tx_comp`` 会按控制器当前空闲缓冲区的数量尽可能多地发送排队的 SDU,然后调用应用的发送完成回调。
- **接收**。一个到来的 SDU 由适配器递交、封装,并作为一个 ``ISO_RX_DATA`` 事件投递;``bt_le_iso_handle_rx_data`` 把它传给引擎中的 ``bt_iso_recv``,后者调用通道的接收回调。
两个方向都汇聚到 ISO 任务:发送完成和接收递交都是普通的排队事件,因此应用的 ISO 回调运行在 ISO 任务上下文、其 4 KB 栈上、在 ISO 锁下,与 :ref:`跨层事件流 <arch-event-flow>` 中所有其他排队事件完全一样。
ESP-BLE-ISO 其余的传输原语是 GATT、GAP 和 L2CAP。与组件其余部分一样,与主机无关的 API(``bt_gatt_*``、``bt_l2cap_*`` 和各 GAP 辅助函数)位于 ``host/common`` 中,并分发给所编译的那个适配器。
GATT
------------------------
GATT 横跨\ **客户端**\ 侧(本设备在对端进行发现、读、写和订阅)、**服务端**\ 侧(本设备暴露供对端访问的属性),以及限定二者的 ATT **MTU 交换**。这三者都在 ``host/common/gatt.c`` 中基于当前激活的适配器实现。
客户端
~~~~~~~~~~~~~~~~~~~~~~~~
``host/common/gatt.c`` 中与主机无关的客户端 API —— ``bt_gatt_discover``、``bt_gatt_read``、``bt_gatt_write``、``bt_gatt_write_without_response_cb``、``bt_gatt_subscribe`` 和 ``bt_gatt_unsubscribe`` —— 分发到当前激活适配器的 ``bt_le_*_gattc_*`` 实现。两个适配器以截然不同的机制支撑同一套 API:
.. list-table:: 各主机的 GATT 客户端支撑
:header-rows: 1
:widths: 22 39 39
* - 方面
- Bluedroid
- NimBLE
* - 发现
- BTA GATTC 流程;BTA 拥有已发现的属性缓存。
- 一次性完整遍历属性表,缓存在 ``gatt.db.c`` 中。
* - 流程串行化
- BTA GATTC 自带的按连接队列。
- ``gatt.nrp.c`` 中的 NRP 队列;插入时对 params 和数据进行深拷贝。
* - 结果递交
- 每个 BTA GATT 事件都投递到 ISO 任务。
- 通知和 ``bt_gatt_discover`` 结果投递到 ISO 任务;对端属性表遍历以及读写完成在主机任务上原地运行。
发现
^^^^^^^^^^^^^^^^^^^^^^^^
客户端发现有两条不同的流程,运行在不同的任务上。这种拆分的存在是因为两种主机在是否维护属性缓存上有差异:Bluedroid 的 BTA GATTC 会发现对端的属性表并自动缓存,而 NimBLE 不会 —— 因此移植在 ``gatt.db.c`` 中补上了该缓存。
**遍历对端的属性表(构建缓存)。**\ 当上层调用 ``bt_gattc_disc_start`` 时,NimBLE 适配器(``bt_le_nimble_gattc_db_auto_disc``)用 NimBLE 的 ``ble_gattc_disc_*`` 流程对对端执行真正的 ATT 发现,把整张表完整遍历一次 —— 每个主服务、包含服务、特征和描述符,包括每个 CCCD。这些流程回调运行在 **NimBLE 主机任务**\ 上,并填充按连接缓存的数据库;只有在遍历完成后,上层才在 ISO 任务上被通知。在 Bluedroid 上,这一步是隐式的:``bt_le_bluedroid_gattc_disc_start`` 驱动 BTA GATTC,由它执行 ATT 流程并维护自己的缓存。
**响应一项发现请求。**\ 当上层 —— 例如音频规范 —— 为特定属性调用 ``bt_gatt_discover`` 时,NimBLE 适配器把一个发现事件投递到 **ISO 任务**,在那里缓存的层次结构在本地回答该请求、无需进一步的 ATT 流量,并调用调用方的回调。在 Bluedroid 上,等价的结果作为 ISO 任务上的发现事件从 BTA 的缓存中呈现。
订阅生命周期
^^^^^^^^^^^^^^^^^^^^^^^^
订阅以与主机无关的方式跟踪。``bt_gatt_subscribe`` 把每个 ``bt_gatt_subscribe_params`` 记录在一个按连接的列表(``gattc_sub``)上,并且,除非已存在等价的订阅,否则通过适配器写入 CCC。有两点行为值得注意:
- 订阅在 CCC 写完成之前就被追加到列表,因为有些对端在回复 CCC 写之前就发送了第一条通知。
- ``subscribe`` 回调被\ **同步**\ 调用,在调用方的上下文中完成该流程,而不是在 CCC 写响应之后。在 NimBLE 上,这与缓存的数据库相配合:CCCD 句柄已经已知,因此无需发现往返,订阅路径也不会阻塞。
断开时,``bt_le_acl_conn_disconnected_gatt_listener`` 按固定顺序清理:先运行上层的断开回调,然后 ``gattc_sub_clear`` 遍历订阅列表,清除每个表项的句柄和值,并重新初始化该列表。因此上层\ **绝不能**\ 在断开回调内部清零自己的 subscribe params —— 此时这些 params 仍然挂在列表上,而通用层紧接着就会清除它们。
流程串行化
^^^^^^^^^^^^^^^^^^^^^^^^
**背景**:``gatt.nrp.c`` 存在的原因与 ``gatt.db.c`` 相同:它提供一项 BTA GATTC 原生提供、而 NimBLE 没有的服务。ATT 在每条连接上同一时刻只允许一个未完成的请求;BTA GATTC 把这一点隐藏在它自己的按连接流程队列之后,而 NimBLE 把它留给调用方来遵守。
**NRP 的作用**:``gatt.nrp.c`` 中的 NRP(need-response PDU,需响应 PDU)队列是 NimBLE 侧的等价物:一个按连接的单一队列,涵盖每一个需要响应的 PDU —— 客户端的读、写和订阅,加上服务端的指示(在下文服务端一节中描述)—— 因此同一时刻只有一个在途,其余的等待轮到自己。
**入队规则**:对客户端流程,它桥接到上层期望的「params 加回调」模型:``bt_le_nimble_gatt_nrp_insert`` 把一个读、写或订阅入队 —— 队列空闲时立即下发,否则保留它直到在途流程完成。``bt_le_nimble_gatt_nrp_remove`` 完成队首表项并启动下一个;``bt_le_nimble_gatt_nrp_clear`` 在断开时丢弃待处理的表项。读有三种形式 —— 按 UUID、长(分片)和单次 —— 每种都为每个结果递交一个事件,并显式处理流程结束。
**深拷贝**:``bt_le_nimble_gatt_nrp_insert`` 对 params 和任何数据进行深拷贝,因此调用方可以传入一个栈缓冲区并在返回时释放它。
**EATT 注记**:「同一时刻一个在途」的规则遵循 ATT「单一承载、每连接一个未完成请求」的模型。EATT 为每条连接引入多个并发承载,因此支持它将意味着重新审视 NRP 队列,以允许按承载的并发,而非单一流程在途。
回调上下文由此而来。NimBLE 在其主机任务上递交读或写完成,NRP 处理函数在那里、在 ISO 锁下调用应用回调 —— 与 GATT 服务端属性访问相同的主机任务上下文。Bluedroid 则把每个 BTA GATT 完成都投递到 ISO 任务,因此相应的回调在那里运行。这就是 :ref:`回调执行上下文 <arch-callback-context>` 的「读写完成」那一行。
服务端
~~~~~~~~~~~~~~~~~~~~~~~~
``host/common/gatt.c`` 的服务端侧负责注册服务(``bt_gatt_service_register``),提供标准的属性读辅助函数(``bt_gatt_attr_read`` 以及服务、包含服务、特征和 CCC 的各变体)、CCC 写路径(``bt_gatt_attr_write_ccc``、``bt_gatts_sub_changed``),以及出站的通知与指示 API(``bt_gatt_notify_cb``、``bt_gatt_indicate``)。两种主机之间差异最大的行为是入站属性访问如何完成,这正是 :ref:`回调执行上下文 <arch-callback-context>` 中首次描述的那个例外:
- Bluedroid 把读或写投递到 ISO 任务;已注册的属性回调在那里运行,响应则通过 BTA 异步返回给对端,并由一个专用的服务端信号量协调。
- NimBLE 在主机任务上原地运行属性回调,因为它的访问回调必须同步返回值。
出站传输同样遵循各主机的模型。在 NimBLE 上,通知是一次同步的缓冲区复制与下发,而指示 —— 它必须等待对端的确认 —— 则通过 NRP 排队,并在确认到达时完成。在 Bluedroid 上,二者都经过 BTA。无论哪种情况,交给 notify 或 indicate 的负载都会在调用返回前被复制,因此调用方可以立即重用它们的缓冲区。
.. _arch-iso-mtu:
MTU 交换
~~~~~~~~~~~~~~~~~~~~~~~~
ATT MTU 交换让两个对端把 ATT MTU 从其 23 字节的默认值抬高,以便单个 PDU 能承载更大的属性值。它是一个 GATT 流程:**客户端**\ 发送 ATT Exchange MTU Request,**服务端**\ 响应,两个对端首选值中较小的那个成为生效的 MTU。这些 GATT 角色独立于 GAP 的 central/peripheral 角色 —— 一个设备通常同时运行客户端和服务端,无论是哪一侧打开了连接 —— 因此这个行为最好按 GATT 角色来理解。
**作为 GATT 服务端。**\ 两种主机始终响应对端的 ATT Exchange MTU Request 并接受协商出的值。这无需应用做任何事,且与 GAP 角色无关。
**作为 GATT 客户端。**\ 设备是否自行抬高 MTU,取决于主机协议栈,并且在 Bluedroid 上取决于 GAP 角色:
- 在 Bluedroid 上这是自动的:当一个 GATT 客户端连接打开时,GATTC 适配器发起该交换(``handle_gattc_open_event`` 调用 ``BTA_GATTC_ConfigureMTU``,在 ``BTA_GATTC_Enh_Open`` 路径内部)。实践中,这会为一个 **central**\ 触发;而对于一个 **peripheral**,GATT 客户端的打开发生在发现启动路径内部,而后者本身又被 MTU 更新事件所阻挡 —— 一个使其无法发起的循环依赖。
- 在 NimBLE 上,组件不发起任何东西,因此由应用用 ``ble_gattc_exchange_mtu()`` 触发它 —— 通常在安全变更事件之后紧接着调用。在 Bluedroid 上,同一个调用对 MTU 而言是空操作,因为适配器已经交换过它了。
由于 GATT 服务端只会响应,一次交换 —— 由先充当客户端的那一侧发起 —— 就为整条连接确定了 MTU;同一条连接上同时也是客户端的对端会重用已协商的值,而不是再运行一次交换。设备所提供的首选值,以及某个规范为何会把它抬高到 ATT 默认值之上,由传输层之上的那一层设定:对于蓝牙 LE Audio,参见 :ref:`ATT MTU <arch-audio-mtu>`。
.. _arch-l2cap:
L2CAP(草案)
------------------------
.. warning::
L2CAP 面向连接的通道目前是\ **早期草案,尚未正式支持**。该实现尚不完整 —— 它只存在于 NimBLE 上(``host/adapter/nimble/l2cap.c``),且\ **没有 Bluedroid 支持** —— 其 API 和行为都是临时的,可能会改变。请勿在生产环境中依赖该层。
``host/common/l2cap.c`` 提供基于信用(credit-based)的 L2CAP 面向连接的通道:一个服务端注册表(``bt_l2cap_server_register``)、出站通道操作(``bt_l2cap_chan_connect``、``bt_l2cap_chan_disconnect``、``bt_l2cap_chan_send``),以及主机在通道被接受、连接、断开或收到数据时调用的入站事件处理函数(``bt_le_l2cap_accept``、``bt_le_l2cap_connected``、``bt_le_l2cap_disconnected``、``bt_le_l2cap_received``)。协议栈中唯一的使用方是对象传输服务,它通过一条基于信用的通道搬运批量对象;不使用对象传输的规范从不打开通道。
.. _arch-app-event:
应用事件接口
------------------------
GAP 和 GATT 事件不会从适配器直达应用。它们会经过 ``host/common/app/gap.c`` 和 ``host/common/app/gatt.c`` 中一个很薄的应用事件接口 —— GAP 和 GATT 事件离开和进入应用的唯一一点。下面的段落跟随一个入站事件从适配器一路向上到应用回调,然后是反向的注入路径。
**注册。**\ 应用通过 ``bt_le_gap_app_cb_register`` 和 ``bt_le_gatt_app_cb_register`` 为每个类别恰好注册一个回调。每个类别只保留一个回调指针,因此所有 GAP 事件有一个应用汇聚点,所有 GATT 事件有一个。
**来源 —— 适配器到 ISO 任务。**\ 在接口能递交一个事件之前,适配器先从主机获取它并把它投递到 ISO 任务队列。对 GAP,Bluedroid 适配器在 ``bt_le_bluedroid_gap_init`` 中注册一个 BTA/BTM GAP 回调;该回调运行在 BTU 任务上,构造一个事件,并用 ``bt_le_bluedroid_gap_post_event`` 入队。NimBLE 适配器提供一个 ``ble_gap_event`` 回调,其 ``*_cb_safe`` 包装函数获取 ISO 锁并投递该事件。GATT 事件由 GATT 适配器以相同方式获取,已在上文 GATT 中详述。除此之外 GAP 本身没有引擎 —— 它只是覆盖在其他模块所拥有的状态之上的一个事件接口:``conn.c`` 中的 ACL 连接表(ACL 连接与断开、安全与身份变更、绑定删除),以及 ``scan.c`` 中的扫描和周期性广播同步状态(扩展扫描报告、PA 同步建立、丢失与报告、BIGInfo)。
**递交 —— ISO 任务到应用。**\ 当 ISO 任务出队一个 GAP 或 GATT 事件时,它调用 ``bt_le_gap_handle_event`` 或 ``bt_le_gatt_handle_event``(来自 ``task.c``)。它们按事件类型分发,构造一个有类型的 ``bt_le_gap_app_event`` 或 ``bt_le_gatt_app_event``,调用已注册的回调,并释放排队的负载。由于这运行在 ISO 任务内部,应用回调在 ISO 任务上下文、在 ISO 锁下执行 —— 即 :ref:`回调执行上下文 <arch-callback-context>` 中描述的那个上下文。递交的 GAP 事件包括 ACL 连接与断开、安全与身份变更、绑定删除、扩展扫描报告、周期性广播同步状态和 BIGInfo;递交的 GATT 事件包括 ATT MTU 变更、GATT 客户端发现完成和 GATT 服务端订阅变更。
**注入 —— 应用到 ISO 任务。**\ 反方向让应用把一个事件投递到 ISO 任务,通过 ``bt_le_gap_app_post_event``(公开暴露为 ``esp_ble_iso_gap_app_post_event``)。它因主机而异:在 Bluedroid 上它转发给 ``bt_le_bluedroid_gap_post_event``,在 NimBLE 上转发给 ``bt_le_nimble_gap_post_event``。在这些组件出厂的配置下,注入路径只在 NimBLE 上使用。
.. mermaid::
flowchart TB
ADP["适配器<br/>(主机事件)"]
T["ISO 任务队列"]
AIF["app/ 接口<br/>(gap.c, gatt.c)"]
APP["应用<br/>(一个 GAP 和<br/>一个 GATT 回调)"]
ADP -->|投递原始事件| T
T -->|出队并分发| AIF
AIF -->|有类型的应用事件| APP
APP -->|调用 app_post_event| AIF
AIF -->|注入队列| T
**接口为何如此设计。**\ 三个目标驱动了这个设计:
- **无论哪种主机都是同一个事件模型。**\ 适配器把每个原生事件 —— Bluedroid 上的 BTA/BTM 回调、NimBLE 上的 ``ble_gap_event`` —— 在它到达应用之前归一化成同一个有类型的 ``bt_le_gap_app_event`` / ``bt_le_gatt_app_event``。因此上层 —— 示例和音频规范 —— 处理一组完全相同的事件,从不针对主机协议栈做分支。这是该接口最重要的特性:在两种主机之间迁移一个应用,无需改动其事件处理。
- **内部使用方也会收到该事件。**\ 有些事件不仅供应用使用:``host/iso/iso.c`` 中的 ISO 引擎和预构建的音频库也会消费它们 —— 一次周期性广播同步及其 BIGInfo 会驱动一次 BIG 同步,而连接、断开和安全变更会驱动各规范。因此接口保证一份副本到达 ISO 任务,内部工作在那里于 ISO 锁下运行。在 Bluedroid 上,BTU 任务的回调直接投递它;在 NimBLE 上,应用用 ``esp_ble_iso_gap_app_post_event`` 把它收到的事件转发进 ISO 任务(参见 :ref:`各主机集成差异 <arch-iso-host-diff>`)。
- **与普通 BLE 应用共存。**\ 向 ISO 任务投递一份副本并不会消耗掉该事件。在 Bluedroid 上,同一个回调仍会把原生事件转发给 Bluedroid 的应用回调层(BTC),因此一个同时通过 ``esp_ble_gap_register_callback`` 注册的应用,会继续收到它用于自己的、非音频的 BLE 工作;在 NimBLE 上,应用已经在它的 ``ble_gap_event`` 回调内部,可以继续在那里处理它需要的其他任何事情。ESP-IDF 蓝牙 LE Audio 应用和一个传统 BLE 应用可以在一个设备上并行运行。
.. important::
这些应用和规范回调运行在 **ISO 任务上**\ (参见 :ref:`回调执行上下文 <arch-callback-context>`),因此回调必须快速返回。在其中阻塞、休眠、等待 I/O 或运行长时间计算,会拖住那个唯一的事件循环,而所有其他 GAP、GATT 以及 —— 最关键的 —— ISO 数据事件都依赖它,这会直接表现为音频延迟或丢失。请把任何耗时工作交给另一个任务。
.. _arch-iso-api:
公共 API
------------------------
到目前为止描述的一切都是内部实现。应用只看到一个公共头文件 ``api/include/esp_ble_iso_common_api.h``,它把传输层暴露为一个小巧的 ``esp_ble_iso_*`` API,用于纯 ISO 用例 —— 不带蓝牙 LE Audio 规范的 CIS 或 BIS。
形态与约定
~~~~~~~~~~~~~~~~~~~~~~~~
公共 API 是覆盖在 :ref:`传输层 <arch-iso-transport>` ISO 引擎之上的一层薄外观:
- **不透明类型**。``esp_ble_iso_chan_t``、``esp_ble_iso_cig_t`` 和 ``esp_ble_iso_big_t`` 等公共类型是内部 ``bt_iso_*`` 结构体的 typedef,``esp_ble_conn_t`` 是 ``bt_conn`` 的 typedef。应用把它们作为不透明句柄持有。
- **错误码。**\ 每个函数返回 ``esp_err_t``,而非引擎内部使用的负 ``errno`` 值。
- **入口加锁。**\ 每次调用都经由其 ``_safe`` 包装函数进入引擎,因此公共 API 是获取全局 ISO 锁的最外层;应用自己从不管理该锁。
- **单次初始化**。``esp_ble_iso_common_init`` 接收一个 ``esp_ble_iso_init_info_t``,其唯一字段是应用的 GAP 回调;此后 ISO 事件通过该回调或通过各通道的操作递交。
功能分组
~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: ESP-BLE-ISO 公共 API 按用途分类
:header-rows: 1
:widths: 24 42 34
* - 分组
- 代表性函数
- 用途
* - 初始化
- ``esp_ble_iso_common_init``
- 注册 GAP 回调并启动传输层。
* - CIS —— central
- ``esp_ble_iso_cig_create``,\ |br|\ ``esp_ble_iso_cig_reconfigure``,\ |br|\ ``esp_ble_iso_cig_terminate``,\ |br|\ ``esp_ble_iso_chan_connect``
- 配置一个连接等时组并建立其各条流。
* - CIS —— peripheral
- ``esp_ble_iso_server_register``,\ |br|\ ``esp_ble_iso_server_unregister``
- 接受入站的连接等时流。
* - BIG —— broadcaster
- ``esp_ble_iso_big_ext_adv_add``,\ |br|\ ``esp_ble_iso_big_create``,\ |br|\ ``esp_ble_iso_big_terminate``,\ |br|\ ``esp_ble_iso_big_register_cb``
- 通过一个广播集广播一个等时组。
* - BIG —— receiver
- ``esp_ble_iso_big_sync``
- 同步到一个广播等时组。
* - 数据通路
- ``esp_ble_iso_setup_data_path``,\ |br|\ ``esp_ble_iso_remove_data_path``,\ |br|\ ``esp_ble_iso_chan_send``,\ |br|\ ``esp_ble_iso_chan_send_ts``
- 把一条流绑定到控制器的 ISO 数据通路并搬运 SDU。
* - 信息
- ``esp_ble_iso_chan_get_info``,\ |br|\ ``esp_ble_iso_chan_get_tx_sync``
- 查询通道与发送定时信息。
* - 辅助
- ``esp_ble_iso_data_parse``
- 解析长度-类型-值(LTV)编码的数据。
这些与 :ref:`ISO 子系统 <arch-iso>` 中的引擎操作一一对应;公共层只增加了锁、错误转换和不透明 typedef。
.. _arch-iso-host-diff:
各主机集成差异
~~~~~~~~~~~~~~~~~~~~~~~~
公共 API 中有两部分专门因为两种主机以不同方式路由连接和 GAP 事件而存在。二者解决同一个问题 —— 让引擎看到应用所发起连接的事件 —— 各以其主机所要求的方式:
- ``esp_ble_iso_gap_app_post_event`` **仅在 NimBLE 上需要**。当应用在 NimBLE 上发起一个连接或扫描时,主机把 GAP 事件递交给应用注册的回调;应用用这个函数把它们转发进引擎。在 Bluedroid 上,引擎安装自己的 BTM GAP 回调并直接捕获事件,因此无需转发。
- ``esp_ble_iso_bluedroid_get_gattc_if`` **仅用于 Bluedroid**。它返回引擎内部的 BTA GATTC 接口,应用在发起连接时把它传给 ``esp_ble_gattc_open``,使由此产生的 ACL 事件路由回引擎,从而避免第二次 BTA GATTC 注册。NimBLE 的对应物是上面的事件转发。
@@ -0,0 +1,508 @@
.. _arch-audio:
ESP-BLE-AUDIO
=============
:link_to_translation:`en:[English]`
.. |br| raw:: html
<br/>
.. contents:: 目录
ESP-BLE-AUDIO 直接位于 ESP-BLE-ISO 之上,实现蓝牙 LE Audio 的各项规范和服务。它是 ESP-IDF 蓝牙 LE Audio 应用实际编程所面向的那一层,通过 ``esp_ble_audio_*`` API 访问。
组件布局
------------------------
该组件有两类截然不同的代码,而这一区分对本文档很重要:
- **开源、本文涵盖**。公共 API 头文件(``api/include/esp_ble_audio_*_api.h``)、``host/adapter/bluedroid/profiles`` 和 ``host/adapter/nimble/profiles`` 下的 GATT 服务适配器、``host/services/ots`` 下的对象传输服务,以及 ``host/common/init.c`` 中的初始化编排。
- **预构建、不在范围内**。ESP-IDF 蓝牙 LE Audio 的规范与控制器逻辑 —— 单播与广播客户端,音量、麦克风、媒体和通话控制,集合协调,以及顶层规范 —— 以预构建的、按目标芯片的库形式提供(``lib/lib/<target>/libble_audio.a``,用 ``add_prebuilt_library`` 链接)。本文档不描述其内部实现;它描述围绕它的公共契约,以及在重要之处,一个已注册回调运行在哪个任务上。
分层
------------------------
.. mermaid::
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 40, 'subGraphTitleMargin': {'top': 6, 'bottom': 14}}}}%%
flowchart TB
APP["应用"]
API["ESP-BLE-AUDIO 公共 API<br/>(esp_ble_audio_*)"]
subgraph AUDIO["ESP-BLE-AUDIO"]
direction LR
LIB["预构建的规范与<br/>控制器库"]
SVC["GATT 服务适配器<br/>(各主机)"]
OTS["对象传输服务"]
LIB ~~~ SVC
LIB ~~~ OTS
end
ISO["ESP-BLE-ISO 传输层<br/>(GATT, GAP, ISO, L2CAP, ISO 任务)"]
APP --> API --> AUDIO --> ISO
.. _arch-audio-svc-ctrl:
服务与控制器
------------------------
蓝牙 LE Audio 的功能分为两类构建块,组件也反映了这一划分:
- **GATT 服务**\ 是对端进行读、写和订阅的属性表。它们在开源的各主机适配器(``host/adapter/*/profiles``)中实现,每个服务一个源文件,构建在 ESP-BLE-ISO 的 GATT 服务端层之上。按其所属规范分组(如蓝牙 LE Audio 规范中那样),这组服务是 PACS、ASCS 和 BASS(BAP),MCS(MCP),TBS(CCP),CSIS(CSIP),MICS(MICP),VCS(VCP),CAS(CAP),TMAS(TMAP)和 HAS(HAP)。
- **规范客户端与控制器**\ 是驱动这些服务并编排各条流的状态机:基本音频规范和通用音频规范,音量、麦克风、媒体和通话控制,协调集识别,以及顶层的电话与媒体、游戏、公共广播规范。这部分逻辑位于预构建库中;应用通过对应的 ``esp_ble_audio_*_api.h`` 头文件访问它。
GATT 服务在 :ref:`GATT 服务 <arch-audio-services>` 中详述,客户端与控制器在 :ref:`客户端与控制器 <arch-audio-profiles>` 中详述。
规范回调运行在何处
------------------------
一个已注册的 ESP-IDF 蓝牙 LE Audio 回调运行在递交底层传输事件的那个任务上,因此 :ref:`回调执行上下文 <arch-callback-context>` 中的规则直接适用:
- 由\ **通知**\ 驱动的回调 —— 例如作为 GATT 通知到达的 ASCS 或音量状态变更 —— 在两种主机上都运行于 ISO 任务。
- 由\ **GATT 客户端读或写完成**\ 驱动的回调 —— 例如读取对端的 PAC 记录 —— 在 Bluedroid 上运行于 ISO 任务,但在 NimBLE 上运行于\ **NimBLE 主机任务**。
- 由对\ **本地服务的入站属性写**\ 驱动的回调 —— 例如客户端配置一个 ASE —— 在 Bluedroid 上运行于 ISO 任务,但在 NimBLE 上运行于\ **NimBLE 主机任务**。
其后果贯穿整个协议栈:在 ISO 任务上,回调共享 4 KB 栈,绝不能阻塞事件循环;而在 NimBLE 主机任务上,它在主机的属性路径内部执行。无论哪种情况,全局 ISO 锁都被持有,因此无论回调运行在哪个任务上,规范状态都保持一致。
.. _arch-audio-services:
GATT 服务
------------------------
蓝牙 LE Audio 通过一组固定的 GATT 服务暴露其能力和状态。这些是各规范的服务端侧 —— 对端进行读、写和订阅的属性表 —— 并且,如 :ref:`服务与控制器 <arch-audio-svc-ctrl>` 中所确立的,它们是 ESP-BLE-AUDIO 的开源部分:``host/adapter/bluedroid/profiles`` 和 ``host/adapter/nimble/profiles`` 下每个服务一个源文件。每个服务背后的状态,以及其控制点的命令处理,位于预构建库中;本节涵盖这些服务及其适配器,而非那部分逻辑。
.. _arch-audio-adapter:
服务适配器模式
~~~~~~~~~~~~~~~~~~~~~~~~
每个服务都以相同的方式实现。预构建库拥有服务属性表的规范化、与主机无关的定义(一个 ``bt_gatt_service``);开源适配器把该表注册到当前激活主机的 GATT 服务端,然后把主机分配的句柄回填到库的定义上,以便库在通知或指示时能寻址自己的属性。两种主机在如何完成注册上有差异:
- **NimBLE** 适配器以 NimBLE 的原生形式(一个 ``ble_gatt_svc_def``)第二次声明该服务,用 ``ble_gatts_add_svcs`` 注册它,检查其特征与库的定义相符,并通过一个 ``*_attr_handle_set`` 步骤把分配的句柄映射回去。注册是同步、单阶段的。
- **Bluedroid** 适配器把库的 ``bt_gatt_service`` 交给一个共享的 ``bt_le_bluedroid_svc_init`` 辅助函数,由它驱动 BTA GATTS。由于 BTA 以两个异步阶段注册一个服务,每个 Bluedroid 服务适配器都暴露一对匹配的 ``*_init`` 和 ``*_start``,并用 GATT 服务端层的服务端信号量协调完成。
一旦服务被注册,对端对其属性的访问遵循 :ref:`回调执行上下文 <arch-callback-context>` 的各主机属性访问路径:在 NimBLE 主机任务上同步完成,或在 Bluedroid 上投递到 ISO 任务。
.. _arch-audio-deferred-add:
**何时添加服务(延迟注册)**
**机制**:并非所有服务都在 ``esp_ble_audio_common_init`` 期间被添加到 GATT 表。构建默认定义了 ``BLE_AUDIO_SVC_DEFERRED_ADD``,它会把一个服务保留到应用通过其 ``esp_ble_audio_<profile>_register`` 调用注册对应角色为止 —— 预构建库把该调用转化为该服务的 ``bt_le_<service>_init`` 注册 —— 而不是在 init 期间添加它。
**原因**:一个固件镜像通常编译进了比任何单个应用所用更多的蓝牙 LE Audio 能力:启用一个服务的 Kconfig 角色选项会把它的代码编译进来,但应用可能永远不会担任那个角色。如果每个编译进来的服务都在 init 时被添加,一个连接上来的对端就会发现、并可能访问一个其背后尚无支撑状态的服务的属性 —— 应用还没注册它,因此预构建库无法对它做出有意义的应答。
**对应用的影响**:延迟添加把一个未使用但已编译进来的能力挡在 GATT 表之外,直到应用主动选用它,因此对端永远只会发现那些被完整支撑的服务。
服务参考
~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: 蓝牙 LE Audio GATT 服务
:header-rows: 1
:widths: 10 24 38 11 17
* - 服务
- 全称
- 关键特征
- 适配器
- 规范
* - PACS
- 已发布音频能力服务(Published Audio Capabilities Service)
- Sink 和 Source PAC 记录、音频位置、可用与支持的音频上下文。
- ``pacs.c``
- BAP
* - ASCS
- 音频流控制服务(Audio Stream Control Service)
- Sink 和 Source 音频流端点(ASE)以及 ASE 控制点。
- ``ascs.c``
- BAP
* - BASS
- 广播音频扫描服务(Broadcast Audio Scan Service)
- 广播接收状态以及广播音频扫描控制点。
- ``bass.c``
- BAP
* - MCS
- 媒体控制服务(Media Control Service)
- 媒体播放器名称与曲目信息、媒体控制点及相关状态。
- ``mcs.c``
- MCP
* - TBS
- 电话承载服务(Telephone Bearer Service)
- 承载信息、通话状态以及通话控制点。
- ``tbs.c``
- CCP
* - CSIS
- 协调集识别服务(Coordinated Set Identification Service)
- 集合身份解析密钥(SIRK)、集合大小、集合成员锁和排名。
- ``csis.c``
- CSIP
* - MICS
- 麦克风控制服务(Microphone Control Service)
- 麦克风静音;包含 AICS。
- ``mics.c``
- MICP
* - VCS
- 音量控制服务(Volume Control Service)
- 音量状态、音量控制点和音量标志;包含 VOCS 和 AICS。
- ``vcs.c``
- VCP
* - CAS
- 通用音频服务(Common Audio Service)
- 标识一个通用音频设备;可包含 CSIS。
- ``cas.c``
- CAP
* - TMAS
- 电话与媒体音频服务(Telephony and Media Audio Service)
- 设备的 TMAP 角色。
- ``tmas.c``
- TMAP
* - HAS
- 助听器访问服务(Hearing Access Service)
- 助听器特性、预设控制点和当前预设索引。
- ``has.c``
- HAP
每个服务都由应用通过其所属规范的 ``esp_ble_audio_*_api.h`` 头文件配置(例如 VCS 用 ``esp_ble_audio_vcp_api.h``,PACS 用 ``esp_ble_audio_pacs_api.h``);这些 API 是后续客户端与控制器参考的一部分。
包含服务与控制点
~~~~~~~~~~~~~~~~~~~~~~~~
有几个服务不是独立的,而是被另一个服务所包含:
- **CAS** 在设备是某个协调集的成员时包含 **CSIS**,因此对端通过通用音频设备发现集合成员关系。
- **VCS** 包含零个或多个 **VOCS**\ (音量偏移控制服务)和 **AICS**\ (音频输入控制服务)实例 —— 每个独立偏移的输出一个 VOCS,每个音频输入一个 AICS。
- **MICS** 包含零个或多个 **AICS** 实例,每个音频输入一个。
因此 AICS 和 VOCS 有公共 API 头文件(``esp_ble_audio_aics_api.h`` 和 ``esp_ble_audio_vocs_api.h``),但没有独立的适配器文件;包含它们的服务把它们作为自己属性表的一部分注册。
有几个服务 —— ASCS、BASS、MCS、TBS、CSIS、VCS 和 HAS —— 暴露一个控制点:一个可写的特征,它携带一条命令,并通过通知受影响的状态特征来应答。命令处理位于预构建库中;适配器只把写入向内传递、把由此产生的通知向外传递,运行在 :ref:`回调执行上下文 <arch-callback-context>` 所描述的任务上。
.. _arch-audio-profiles:
客户端与控制器
------------------------
各规范及其客户端和控制器角色是 ESP-BLE-AUDIO 中以预构建库形式提供的部分。本节在本文档所能达到的层面描述它们:它们的角色、应用驱动它们所用的公共 API,以及它们的回调运行在哪个任务上。内部状态机不在范围内。
一个客户端或控制器角色通过一条连接作用于远端设备 —— 例如一个音量控制器改变某个渲染器的音量 —— 而一个渲染器、设备、成员或服务端角色在本地应答,并由上一节中的某个 GATT 服务支撑。一个应用可以同时担任多个角色。
规范之间的关系
~~~~~~~~~~~~~~~~~~~~~~~~
蓝牙 LE Audio 各规范彼此构建于其上。\ **基本音频规范(BAP)**\ 是基础:它建立并承载音频流,既包括单播(通过连接等时流)也包括广播(通过广播等时流)。\ **控制规范** —— 音量、麦克风、协调集、媒体和通话控制 —— 管理各项功能,每个都由一个 GATT 服务支撑。\ **通用音频规范(CAP)**\ 协调其他规范,使一个操作在一个集合的每个成员上一致地生效。\ **顶层规范** —— 电话与媒体音频(TMAP)、游戏音频(GMAP)、公共广播(PBP)和助听器访问(HAP)—— 是下层各层的既定组合。
.. mermaid::
flowchart TB
TOP["顶层规范 — TMAP、GMAP、PBP、HAP"]
CAP["CAP — 跨协调集的协同控制"]
CTRL["控制规范 — VCP、MICP、CSIP、MCP、CCP"]
BAP["BAP — 单播 (CIS) 与广播 (BIS) 流"]
SVC["蓝牙 LE Audio GATT 服务"]
ISO["ESP-BLE-ISO 传输层"]
TOP --> CAP
CAP --> BAP
CAP --> CTRL
TOP --> CTRL
CTRL --> SVC
BAP --> ISO
规范参考
~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: 蓝牙 LE Audio 规范
:header-rows: 1
:widths: 10 27 45 18
* - 规范
- 全称
- 角色
- API 头文件
* - BAP
- 基本音频规范(Basic Audio Profile)
- 单播客户端与服务端、广播源与广播接收端、广播助手、扫描委托设备。
- ``bap_api.h``
* - CAP
- 通用音频规范(Common Audio Profile)
- 在一个协调集上的发起者、指挥者与切换。
- ``cap_api.h``
* - VCP
- 音量控制规范(Volume Control Profile)
- 音量控制器(客户端)和音量渲染器(服务端)。
- ``vcp_api.h``
* - MICP
- 麦克风控制规范(Microphone Control Profile)
- 麦克风控制器(客户端)和麦克风设备(服务端)。
- ``micp_api.h``
* - CSIP
- 协调集识别规范(Coordinated Set Identification Profile)
- 集合协调者(客户端)和集合成员(服务端)。
- ``csip_api.h``
* - MCP
- 媒体控制规范(Media Control Profile)
- 媒体控制客户端(客户端)和媒体代理(服务端)。
- ``mcc_api.h``、``media_proxy_api.h``
* - CCP
- 通话控制规范(Call Control Profile)
- 通话控制客户端(服务端侧即电话承载服务)。
- ``ccp_api.h``
* - TMAP
- 电话与媒体音频规范(Telephony and Media Audio Profile)
- 组合 CAP、BAP 和各控制规范的角色配置。
- ``tmap_api.h``
* - GMAP
- 游戏音频规范(Gaming Audio Profile)
- 面向低延迟游戏音频的角色配置。
- ``gmap_api.h``
* - PBP
- 公共广播规范(Public Broadcast Profile)
- 在 BAP 广播之上的公共广播通告辅助。
- ``pbp_api.h``
API 形态与角色
~~~~~~~~~~~~~~~~~~~~~~~~
每个规范在其 ``esp_ble_audio_<profile>_api.h`` 头文件中暴露一套小巧、统一的 API:应用为它所担任的角色注册一个回调结构体,并调用函数来启动操作 —— 发现一个对端、配置一条流、设置音量、发起通话。结果和状态变更随后异步地通过该回调到达,运行在 :ref:`回调执行上下文 <arch-callback-context>` 所描述的任务上 —— 多数情况下是 ISO 任务,对于 GATT 读写完成和对本地服务的入站写则是 NimBLE 主机任务。
由于注册和分发在各规范之间是统一的,向一个应用添加一个角色总是同样的形态:启用该角色的 Kconfig 选项、注册其回调结构体、并通过其函数驱动它。角色选项还决定了哪些 GATT 服务被编译进来,正如下面的初始化流程所示。
对象传输(草案)
------------------------
.. warning::
对象传输目前是\ **早期草案,尚未正式支持**。它构建在草案 L2CAP 通道(:ref:`L2CAP <arch-l2cap>`)之上,因此只运行在 NimBLE 上,**不支持 Bluedroid**,其 API 和行为都是临时的,可能会改变。请勿在生产环境中依赖它。
对象传输服务(OTS)通过 :ref:`L2CAP <arch-l2cap>` 中那条基于信用的 L2CAP 通道搬运批量对象 —— 比一次普通 GATT 读所能承载的更大。在蓝牙 LE Audio 中它被媒体控制使用:一个媒体播放器暴露诸如当前曲目分段和分组结构之类的对象,客户端用 OTS 传输它们,经由媒体 API(``esp_ble_audio_mcs_get_ots`` 和媒体控制客户端)访问。
与各规范不同,OTS 是开源的,位于 ``host/services/ots`` 下。它的各个部分是:
- 一个\ **服务端**\ (``ots.c``)和一个\ **客户端**\ (``ots_client.c``);
- **对象动作控制点**\ (``ots_oacp.c``),它承载读、写、创建等对象操作;
- **对象列表控制点**\ (``ots_olcp.c``),它在对象列表中导航;
- 一个\ **对象管理器**\ (``ots_obj_manager.c``)和一个\ **目录列表**\ 对象(``ots_dir_list.c``);
- **L2CAP 传输**\ (``ots_l2cap.c``),它通过基于信用的通道承载对象数据。
.. _arch-init-flow:
初始化流程
------------------------
一个 ESP-IDF 蓝牙 LE Audio 应用用两次调用启动协议栈,与传输层对应:先 ``esp_ble_audio_common_init``,后 ``esp_ble_audio_common_start``。
``esp_ble_audio_common_init`` 运行 ``init.c`` 中的 ``bt_le_audio_init``,它首先与预构建库建立边界 —— 检查共享结构 ABI 是否匹配,并把当前激活的 Kconfig 值推送进库 —— 然后调用主机相关的 ``bt_le_{host}_audio_init``。该主机初始化函数启动标准的 GAP 和 GATT 服务,然后按固定顺序通过各自的适配器注册每个已启用的蓝牙 LE Audio 服务:PACS、ASCS、BASS、TMAS、GTBS、HAS、CSIS 和 CAS,然后是媒体、音量和麦克风服务。每个服务仅在其 Kconfig 角色选项被设置时才编译进来,因此一个构建恰好包含其角色所需的服务。那个固定顺序是这些服务注册的顺序;它们中大多数实际何时进入 GATT 表是另一个问题 —— 默认情况下,添加被延迟到应用按角色的 ``esp_ble_audio_<profile>_register`` 调用,而不是在 init 这里发生(参见 :ref:`延迟注册 <arch-audio-deferred-add>`)。
``esp_ble_audio_common_start`` 运行 ``bt_le_audio_start`` 并分发到主机相关的 start,它首先从 ``start_info`` 注册协调集服务(CSIS 和 CAS),然后提交 GATT 服务端。在 Bluedroid 上,提交是第二个阶段,它真正启动 init 期间注册的 BTA 服务 —— 即 :ref:`服务适配器模式 <arch-audio-adapter>` 中的 ``*_init`` 与 ``*_start`` 配对;在 NimBLE 上,它调用 ``ble_gatts_start``,然后把分配的属性句柄回填到库中。
.. mermaid::
%%{init: {'sequence': {'noteAlign': 'left'}}}%%
sequenceDiagram
participant APP as Application
participant CMN as ESP-BLE-AUDIO (init.c)
participant LIB as Prebuilt library
participant AD as Host adapter
APP->>CMN: esp_ble_audio_common_init (gap_cb, gatt_cb)
CMN->>LIB: 检查 ABI,推送配置
CMN->>AD: bt_le_{host}_audio_init
Note over AD: 启动 GAP/GATT,然后按固定顺序<br/>注册已启用的服务
APP->>CMN: esp_ble_audio_common_start
CMN->>AD: bt_le_{host}_audio_start
Note over AD: 从 start_info 注册 CSIS/CAS,然后提交 GATT 服务端<br/>Bluedroid:第二阶段启动已注册的 BTA 服务<br/>NimBLE:ble_gatts_start,然后回填属性句柄
.. _arch-audio-mtu:
ATT MTU
~~~~~~~~~~~~~~~~~~~~~~~~
蓝牙 LE Audio 依赖一个足够大的 ATT MTU 来承载其控制 PDU。
**规范下限**:基本音频规范(BAP)要求的最低 ATT MTU 仅为 64 字节(公共的 ``ESP_BLE_AUDIO_ATT_MTU_MIN``)。
**为何选 128**:64 字节不足以同时操作四个 ASE,因此 ESP-IDF 把默认值提高到 128 字节(``BLE_AUDIO_ATT_MTU_MIN``),为蓝牙 LE Audio 所依赖的 ASCS 和 PACS 控制 PDU 留出余量。两种主机都在初始化期间(``bt_le_{host}_audio_init``)把设备的首选 ATT MTU 设为该值,Bluedroid 通过 ``BTA_GATT_SetLocalMTU``、NimBLE 通过 ``ble_att_set_preferred_mtu``。
**协商规则**:该值是设备作为客户端时提供、作为服务端时接受的值,因此最终生效的 MTU 是两个对端首选值中较小的那个。
交换本身就是 :ref:`MTU 交换 <arch-iso-mtu>` 中描述的那个 GATT 流程:客户端发送请求,服务端响应,一次协商管辖整条连接。在通常的蓝牙 LE Audio 拓扑中 —— 手机作为 central、ESP 设备作为 peripheral —— 每个设备都\ **同时**\ 运行一个 GATT 客户端和一个 GATT 服务端,因为 GATT 角色独立于 GAP 角色。然而单次 MTU 交换覆盖整条连接:手机的客户端发起它,ESP 的服务端响应,随后那一个协商出的值便管辖它们之间所有的 ATT 流量 —— 无论某一时刻哪一侧充当客户端或服务端:
.. mermaid::
sequenceDiagram
box transparent 手机 (GAP central)
participant PS as GATT server
participant PC as GATT client
end
box transparent ESP 设备 (GAP peripheral)
participant ES as GATT server
participant EC as GATT client
end
PC->>ES: ATT Exchange MTU Request
ES-->>PC: ATT Exchange MTU Response
Note over ES: 服务端只响应,从不发起
Note over PS,EC: 那一个协商出的 MTU(两侧的较小值)<br/>管辖所有 ATT 流量,双向
PC->>ES: 服务发现
PC->>ES: 订阅(CCCD),然后 PACS / ASCS 读、写
EC->>PS: 服务发现(peripheral 作为客户端,复用同一 MTU)
另外两个角色 —— 手机的 GATT 服务端和 ESP 的 GATT 客户端 —— 也存在于同一条连接上:ESP 的客户端可以对手机的服务端运行自己的服务发现,但这两个角色都不会运行第二次 MTU 交换;二者都重用手机的客户端已经协商好的值。由于 ESP peripheral 只响应、从不发起 MTU 交换,那一次协商的节奏由 central 把控,而非由 peripheral 的主机协议栈。
端到端流程
------------------------
下面的四条流程把各层串联起来,对应两个单播角色和两个广播角色。它们刻意保持高层次 —— 每个箭头可能代表多次交换 —— 并展示每一步由哪个组件负责。
单播发起端(Unicast Initiator)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
单播发起端(Initiator)连接到一个接受端(Acceptor),配置其音频流端点,建立一条连接等时流,并开始发送音频:
.. mermaid::
sequenceDiagram
participant APP as Application (initiator)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>ISO: 扫描并连接(GAP)
APP->>ISO: 启动 GATT 发现
AUD->>ISO: 发现 PACS 和 ASCS,订阅 ASE
AUD->>ISO: 配置 ASE(写 ASE 控制点)
AUD->>ISO: 创建 CIG 并连接 CIS
ISO->>CTRL: LE Set CIG Parameters, Create CIS
CTRL->>ISO: CIS Established 事件
AUD->>ISO: 建立 ISO 数据通路(输入)
APP->>CTRL: 发送 ISO SDU
CTRL->>ISO: ISO 发送完成
Note over APP,CTRL: 后续 SDU 重复发送 / 发送完成的循环
流的建立 —— 发现 PACS 和 ASCS、配置各 ASE、创建 CIG、连接 CIS 和建立数据通路 —— 是由 ESP-BLE-AUDIO 规范库在 ESP-BLE-ISO 的 GATT 和 ISO 层之上驱动的,而非由应用直接驱动。应用只通过 ``esp_ble_audio_*`` API 驱动高层流程 —— 连接、启动发现、启动流和发送音频 —— 而 ESP-BLE-AUDIO 代为执行底层的 ESP-BLE-ISO 操作。
单播接受端(Unicast Acceptor)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
单播接受端(Acceptor)进行广播,接受来自发起端的连接,并在接收音频之前让发起端配置其音频流端点:
.. mermaid::
sequenceDiagram
participant APP as Application (acceptor)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>ISO: 广播(GAP,音频通告)
CTRL->>ISO: ACL 连接已建立(peripheral)
CTRL->>ISO: 远端 initiator 写 ASE 控制点
ISO->>AUD: ASCS 服务端应用 config / QoS / enable
AUD->>APP: config / QoS / enable 回调
APP->>AUD: 响应(接受 / 拒绝 + QoS 偏好)
AUD->>ISO: 通知 ASE 状态和控制点结果
ISO->>CTRL: 发送给远端 initiator
CTRL->>ISO: CIS Established 事件
AUD->>ISO: 建立 ISO 数据通路(输出)
CTRL->>ISO: ISO SDU
ISO->>APP: ISO 任务上的接收回调
Note over APP,CTRL: 后续 ISO SDU 重复接收 / 接收回调的循环
与发起端不同,接受端是被动的。远端发起端通过写 ASE 控制点驱动 ASCS 状态机,接受端的 ESP-BLE-AUDIO ASCS 服务端把每一步 —— config、QoS、enable —— 作为回调呈现给应用。每个回调返回应用的响应 —— 接受或拒绝,外加编解码器配置时的 QoS 偏好 —— ASCS 服务端把它作为更新后的 ASE 状态和控制点结果通知回发起端。连接等时流由发起端建立(接受端是 peripheral);ESP-BLE-AUDIO 建立 ISO 数据通路,并通过 ISO 任务上的接收回调把收到的 SDU 递交给应用。与发起端一样,应用从不直接调用 ESP-BLE-ISO 的 ISO API。
广播源(Broadcast Source)
~~~~~~~~~~~~~~~~~~~~~~~~~~
广播源(Broadcast Source)配置其音频,启动一条承载 BASE 的周期性广播序列,并在没有任何连接的情况下把音频发送进一个广播等时组:
.. mermaid::
sequenceDiagram
participant APP as Application (broadcast source)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>AUD: 创建广播 Source(codec、QoS、BASE)
APP->>ISO: 启动扩展和周期性广播(GAP,承载 BASE)
APP->>AUD: 启动广播 Source
AUD->>ISO: 创建 BIG
ISO->>CTRL: LE Create BIG
CTRL->>ISO: BIG Complete 事件
AUD->>ISO: 建立 ISO 数据通路(输入)
AUD->>APP: 流已启动回调
APP->>CTRL: 发送 ISO SDU
CTRL->>ISO: ISO 发送完成
Note over APP,CTRL: 后续 SDU 重复发送 / 发送完成的循环
应用通过 ``esp_ble_audio_*`` API 驱动高层流程 —— 创建广播源、启动广播、启动广播源和发送音频 —— 而 ESP-BLE-AUDIO 构建 BASE、在 ESP-BLE-ISO 的 ISO 子系统之上创建 BIG,并建立数据通路。与单播发起端一样,底层的 ESP-BLE-ISO 操作由 ESP-BLE-AUDIO 执行,而非应用。广播源没有连接也没有 GATT:它只进行广播和发送。
广播接收端(Broadcast Sink)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
广播接收端(Broadcast Sink)同步到广播端的周期性序列,加入其广播等时组并接收音频:
.. mermaid::
sequenceDiagram
participant APP as Application (broadcast sink)
participant AUD as ESP-BLE-AUDIO
participant ISO as ESP-BLE-ISO
participant CTRL as Controller
APP->>ISO: 扫描并同步到周期性广播
CTRL->>ISO: BIGInfo 报告
AUD->>ISO: BIG 同步(若加密则带广播码)
ISO->>CTRL: LE BIG Create Sync
CTRL->>ISO: BIG Sync Established 事件
AUD->>ISO: 建立 ISO 数据通路(输出)
CTRL->>ISO: ISO SDU
ISO->>APP: ISO 任务上的接收回调
Note over APP,CTRL: 后续 ISO SDU 重复接收 / 接收回调的循环
应用驱动高层 —— 周期性广播同步(通过 GAP)和广播接收端的启动 —— 而 ESP-BLE-AUDIO 的广播接收端规范执行 BIG 同步和数据通路。两者都依托 ESP-BLE-ISO:用它的扫描层做 PA 同步和 BIGInfo 报告,用它的 ISO 子系统做 BIG 同步和数据通路。如果该组是加密的而广播码错误,控制器仍会同步,但流很快会因消息完整性检查失败,因此应用应把同步后早期的失败视为很可能是广播码错误。
.. _arch-audio-api:
公共 API
------------------------
应用通过 ``esp_ble_audio_*`` API 驱动 ESP-IDF 蓝牙 LE Audio 实现。它有两个接口面:一个公共头文件 ``api/include/esp_ble_audio_common_api.h``,用于启动和横切的入口点;以及每个规范和服务一个 ``<profile>_api.h`` 头文件,用于角色相关的操作 —— 即 API 的主体,已在 :ref:`客户端与控制器 <arch-audio-profiles>` 中编目。
形态与约定
~~~~~~~~~~~~~~~~~~~~~~~~
公共 API 位于 ESP-BLE-ISO :ref:`传输层 <arch-iso-transport>` 和预构建规范库之上,并遵循与传输层公共 API 相同的约定:
- **两次调用启动**。``esp_ble_audio_common_init`` 接收一个 ``esp_ble_audio_init_info_t`` —— 一个 GAP 回调和一个 GATT 回调,即 :ref:`应用事件接口 <arch-app-event>` 中那唯一的应用汇聚点。``esp_ble_audio_common_start`` 接收一个 ``esp_ble_audio_start_info_t``,携带要启动的协调集(CSIS)服务实例。这两次调用内部做了什么,是 :ref:`初始化流程 <arch-init-flow>` 的主题:与预构建库的一次握手 —— 共享结构 ABI(Application Binary Interface,应用二进制接口)检查和 Kconfig 配置推送 —— 随后是逐个服务的注册。
- **不透明、与传输层别名的类型**。应用所处理的事件类型 —— ``esp_ble_audio_gap_app_event_t`` 和 ``esp_ble_audio_gatt_app_event_t`` —— 是传输层 ``bt_le_gap_app_event`` / ``bt_le_gatt_app_event`` 的 typedef,而 ``ESP_BLE_AUDIO_GAP_EVENT_*`` / ``ESP_BLE_AUDIO_GATT_EVENT_*`` 代码是传输层代码的别名,因此音频层和 ISO 层呈现同一个事件模型。
- **错误码**。每个函数返回 ``esp_err_t``。
- **按角色注册**。一个规范角色遵循 :ref:`客户端与控制器 <arch-audio-profiles>` 中那个统一的形态:启用该角色的 Kconfig 选项,通过该角色的 ``<profile>_api.h`` 注册其回调结构体,并通过该头文件的函数驱动它。注册一个角色也正是把它的 GATT 服务添加到表中的操作(参见 :ref:`延迟注册 <arch-audio-deferred-add>`)。
功能分组
~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table:: ESP-BLE-AUDIO 公共 API 按用途分类
:header-rows: 1
:widths: 24 42 34
* - 分组
- 代表性函数
- 用途
* - 初始化
- ``esp_ble_audio_common_init``
- 注册 GAP 和 GATT 回调,并启动预构建库、GAP 和 GATT。
* - 启动
- ``esp_ble_audio_common_start``
- 从 ``start_info`` 注册协调集服务(CSIS、CAS),并启动 GATT 服务端(Bluedroid 上的第二个 BTA 阶段)。
* - GATT 发现
- ``esp_ble_audio_gattc_disc_start``
- 在一条连接上启动对对端服务的 GATT 发现。
* - 事件转发
- ``esp_ble_audio_gap_app_post_event``,\ |br|\ ``esp_ble_audio_gatt_app_post_event``
- 把主机的 GAP 和 GATT 事件转发进引擎(仅 NimBLE —— 见下文)。
* - LTV 辅助
- ``esp_ble_audio_data_parse``,\ |br|\ ``esp_ble_audio_data_get_val``
- 解析蓝牙 LE Audio 通篇使用的长度-类型-值元数据。
* - 按规范的角色
- 各 ``<profile>_api.h`` 的注册与操作函数
- 担任并驱动一个规范角色 —— 即 API 的主体(参见 :ref:`客户端与控制器 <arch-audio-profiles>`)。
各主机集成差异
~~~~~~~~~~~~~~~~~~~~~~~~
与 ESP-BLE-ISO 一样,唯一因主机而异的入口点是事件转发函数,它们存在的原因相同 —— 让引擎看到应用所驱动连接的 GAP 和 GATT 事件:
- ``esp_ble_audio_gap_app_post_event`` 和 ``esp_ble_audio_gatt_app_post_event`` **仅在 NimBLE 上需要**:主机把 GAP 和 GATT 事件递交给应用注册的回调,应用用它们把事件向内转发。在 Bluedroid 上,适配器安装自己的 BTA/BTM 回调并直接捕获事件,因此无需转发 —— ``esp_ble_audio_gatt_app_post_event`` 在 Bluedroid 构建中甚至是隐藏的,因此在那里调用它是一个编译期错误。
@@ -0,0 +1,284 @@
.. _ble-audio-architecture:
ESP-IDF 蓝牙 LE Audio 架构
==============================
:link_to_translation:`en:[English]`
本文档描述 ESP-IDF 蓝牙 LE Audio 协议栈,以及它所基于的通用 BLE 传输层的内部架构,涉及两个蓝牙组件:
- **ESP-BLE-ISO** —— 通用的 BLE 传输层。它提供 GATT、GAP、ISO、L2CAP 和 HCI 原语,以及用于串行化所有主机事件的专用处理任务和锁模型。它\ **并非蓝牙 LE Audio 专用** —— 它是一个自包含的传输层,任何上层规范都可以构建于其上,也可以\ **独立运行**:它拥有自己的公共 API(``esp_ble_iso_*``)和自己的初始化流程,因此应用程序可以直接使用它,而无需 ESP-BLE-AUDIO 或其上的任何规范层。
- **ESP-BLE-AUDIO** —— 上层规范层。它实现蓝牙 LE Audio 的各项规范和服务(PACS、ASCS、BASS、BAP、CAP、VCP、MICP、CSIP、MCP、CCP、TMAP 等),并通过公共 ``esp_ble_audio_*`` API 对外暴露。
.. note::
ESP-BLE-AUDIO 目前是 ESP-BLE-ISO 的唯一使用方,但并非唯一的预期使用方,而且它完全不是必需的。
ESP-BLE-ISO 是一个独立组件:它可以单独构建和初始化,并通过其公共 ``esp_ble_iso_*`` API 驱动,用于纯 ISO 场景(CIS 或 BIS,不带任何规范层)—— 参见 :ref:`ESP-BLE-ISO 公共 API <arch-iso-api>`。
该传输层也与规范无关:其 GATT、GAP、ISO、L2CAP 和 HCI 原语都是通用的 BLE 构建块,其他规范 —— 例如未来的 HID-over-ISO —— 预期也会构建在同一组件之上。本文档中凡描述 ESP-BLE-ISO 之处,其行为均适用于任何使用方,而不仅限于蓝牙 LE Audio。
这两个组件都可运行在 **Bluedroid** 或 **NimBLE** 两种 BLE 主机协议栈之一之上,并隐藏在一个公共接口之后。明确揭示两种主机之间的行为差异是本文档的首要目标之一,因为它们决定了回调运行所处的任务上下文、连接建立期间的操作顺序,以及事件的分发方式。
.. note::
本指南面向两类读者。概览部分给出层级层面的思维模型,适用于任何集成两个组件之一公共 API 的开发者(分别参见 :ref:`ESP-BLE-ISO <arch-iso-api>` 与 :ref:`ESP-BLE-AUDIO <arch-audio-api>`)。后续章节则深入到实现层面(任务、队列、锁、各主机的分发路径),面向维护组件本身的开发者。源码以文件路径和符号名引用,而非行号,因此这些引用会随代码演进而保持有效。
.. |br| raw:: html
<br/>
.. contents:: 目录
:local:
:depth: 1
架构概览
----------------
分层结构
~~~~~~~~~~~~~~~~
ESP-IDF 蓝牙 LE Audio 被组织为一组相互协作的分层。每一层只与其正下方的一层通信,所有与主机相关的知识都被限制在每个组件内部的适配器子层中。
.. mermaid::
flowchart TB
APP["应用 / 示例(app 任务)"]
AUDIO["ESP-BLE-AUDIO —<br/>蓝牙 LE Audio 规范与服务<br/>(esp_ble_audio_* API)"]
ISO["ESP-BLE-ISO — 传输原语:<br/>GATT、GAP、ISO、L2CAP、HCI<br/>全部由全局 ISO 锁串行化 —<br/>多数事件运行在 ISO 任务事件循环上"]
HOST["BLE 主机协议栈<br/>(Bluedroid 或 NimBLE)"]
CTRL["BLE 控制器"]
APP <--> AUDIO
AUDIO <--> ISO
ISO <--> HOST
HOST <--> CTRL
ISO <-. ISO 数据通路 .-> CTRL
.. note::
实线链路是控制路径,它在两个方向上都遵循分层结构 —— 命令向下流动,事件向上流动。\ **ISO 数据通路**\ 是例外(虚线链路):在核心规范定义的两个数据通路方向上,它都绕过了主机协议栈。在\ **输入**\ 数据通路(主机到控制器)上,ESP-BLE-ISO 将 SDU 直接写入控制器传输接口,控制器再通过回调将发送完成报告回 ISO 任务;在\ **输出**\ 数据通路(控制器到主机)上,控制器通过同类回调递交接收到的 ISO 数据。两条路径都不经过主机协议栈的 HCI 事件分发,从而使高速率的音频数据通路保持简短。
应用程序只调用公共 ``esp_ble_audio_*`` API(对于纯 ISO 用例则调用 ``esp_ble_iso_*``)。公共 API 之下的一切都是内部实现,可能在不同版本之间发生变化。
两个组件
~~~~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 16 40 44
* - 组件
- 职责
- 关键目录
* - ESP-BLE-ISO
- - ACL 连接管理
- 广播与扫描
- GATT 客户端与服务端
- ISO(CIS 和 BIS)
- HCI 和 L2CAP
- ISO 任务事件循环
- ``esp_ble_iso_*`` API
- - ``host/common``
- ``host/adapter/bluedroid``
- ``host/adapter/nimble``
- ``api/include``
* - ESP-BLE-AUDIO
- - 蓝牙 LE Audio GATT 服务
- 规范客户端与控制器
- 对象传输服务(OTS)
- ``esp_ble_audio_*`` API
- - ``host/common``
- ``host/adapter/bluedroid/profiles``
- ``host/adapter/nimble/profiles``
- ``host/services/ots``
- ``api/include``
.. _arch-dual-host:
双主机设计
----------------
每个组件都被拆分为\ **与主机无关的通用**\ 部分和\ **主机相关的适配器**\ 部分。``host/common`` 下的目录持有一份不依赖主机协议栈的实现,而 ``host/adapter/bluedroid`` 和 ``host/adapter/nimble`` 则持有直接与各主机交互的代码。最终只会编译其中一个适配器,由当前激活的主机选择(``CONFIG_BT_BLUEDROID_ENABLED`` 或 ``CONFIG_BT_NIMBLE_ENABLED``)。通用层定义了两个适配器都需实现的内部接口,因此上层永远无需针对主机协议栈做分支判断。
这两种主机在行为上并不完全一致。它们在回调运行所处的任务、入站事件如何到达 ISO 任务,以及若干流程级细节(MTU 交换、服务发现、ISO 建立)上都有差异,这些将在各分层章节中介绍。结构性差异总结如下;其中对应用开发者最重要的一项 —— 回调运行所处的任务 —— 在 :ref:`回调执行上下文 <arch-callback-context>` 中详述。
.. list-table:: 主机层面的差异
:header-rows: 1
:widths: 26 37 37
* - 方面
- Bluedroid
- NimBLE
* - 引发入站事件的任务
- BTU 任务(BTA GATT 和 GAP 回调在此运行)。
- NimBLE 主机任务。
* - 适配器如何转发事件
- BTA 回调构造一个事件并将其投递到 ISO 任务队列。
- ``*_cb_safe`` 包装函数先获取 ISO 锁;随后大多数事件被投递到 ISO 任务队列。
* - 同步例外
- GATT 服务端的属性访问以异步方式完成:请求被投递到 ISO 任务,响应稍后发送。
- GATT 服务端的属性访问在主机任务上原地(inline)完成,因为 NimBLE 要求同步返回属性值。
* - ISO 任务所在 CPU 核
- 绑定到所配置的 Bluedroid 核。
- 绑定到所配置的 NimBLE 核。
* - ISO 任务优先级
- 与 BTU 任务优先级保持一致。
- 与 NimBLE 主机任务优先级保持一致。
.. note::
ISO 任务被特意赋予与主机协议栈主任务相同的 CPU 核和相同的优先级(参见 ``host/common/include/common/task.h``)。让传输任务运行在与主机相同的优先级上,可使二者协作式调度,而非相互抢占。
组件内部
----------------
各组件的实现细节见下表,涵盖任务、队列、锁以及各主机的分发路径,面向在组件内部工作的维护者:
.. list-table::
:header-rows: 1
:widths: 26 74
* - 页面
- 涵盖内容
* - :ref:`ESP-BLE-ISO <arch-iso-transport>`
- - 并发模型 —— 单一的 ISO 任务与全局锁,以及每个回调运行所处的任务
- 连接管理
- 广播、扫描与周期性广播同步
- HCI 命令路径
- ISO 子系统 —— CIS 和 BIG 状态机,以及 SDU 数据通路
- GATT 客户端与服务端,以及 ATT MTU 交换
- L2CAP(草案)与应用事件接口
- ``esp_ble_iso_*`` 公共 API
* - :ref:`ESP-BLE-AUDIO <arch-audio>`
- - 开源规范适配器与预构建库
- GATT 服务与服务适配器模式
- 规范客户端与控制器
- 对象传输(草案)
- 初始化流程与 ATT-MTU 取值
- 端到端单播与广播流程
- ``esp_ble_audio_*`` 公共 API
.. toctree::
:hidden:
:maxdepth: 1
ble-audio-architecture-iso
ble-audio-architecture-lea
横切关注点
----------------
本节汇集跨越两个组件、并在整个实现中反复出现的不变式和实际关注点。
主机对等
~~~~~~~~~~~~~~~~
最重要的维护不变式是:两个主机适配器在行为上保持等价。它们之上的通用层假定行为完全相同,因此对一个适配器的连接、清理、发现或事件路径的改动,必须在另一个上镜像 —— 否则上层会因主机不同而表现不同。在两者确实无法匹配之处,这种不对称是有意为之的,且其完整集合很小:
.. list-table:: 有意的各主机差异
:header-rows: 1
:widths: 30 35 35
* - 方面
- Bluedroid
- NimBLE
* - 引发入站事件的任务
- BTU 任务
- NimBLE 主机任务
* - GATT 服务端属性访问
- 投递到 ISO 任务;响应通过 BTA 异步发送
- 在主机任务上原地;值同步返回
* - GATT 客户端读/写完成
- 投递到 ISO 任务
- 在主机任务上原地
* - HCI ISO 命令
- 私有的直连 HCI 路径,带自己的完成信号量
- 类型化的 ``ble_hs_hci_*`` 辅助函数
* - GATT 服务注册
- 两阶段且异步(``*_init`` 然后 ``*_start``)
- 单阶段 ``ble_gatts_add_svcs``
* - L2CAP 面向连接的通道
- 未实现(TODO)
- 已实现(草案)
* - 对象传输服务(OTS)
- 未实现(构建在 L2CAP 之上)
- 已实现(草案)
* - 连接发起的事件路由
- 共享引擎的 BTA GATTC 接口(``esp_ble_iso_bluedroid_get_gattc_if``)
- 把 GAP 事件转发进引擎(``*_gap_app_post_event``)
超出这些范围的任何差异都需要同样的论证,并记录在引入它的地方。
层边界
~~~~~~~~~~~~~~~~
该架构依赖几条调用者和维护者都应尊重的边界:
- **公共 API 是唯一稳定的接口面**。``esp_ble_iso_*`` 和 ``esp_ble_audio_*`` API 之下的一切都是内部实现,可能在不同版本之间改变。
- **两种 BLE 主机协议栈是被适配,而非被修改**。适配器层的存在正是为了让组件永不改动 Bluedroid 或 NimBLE 本身;主机行为被视为既定。
- **规范与控制器逻辑是一个具有固定 ABI 的预构建库**。开源代码只通过在初始化时校验的共享函数表和配置表与它交互;它的内部实现不属于这个契约。
常见陷阱
~~~~~~~~~~~~~~~~
:ref:`并发与线程安全 <arch-concurrency>` 中的并发模型有一些容易弄错的实际后果:
- **不要在回调中阻塞**。大多数回调运行在 ISO 任务上;在那里阻塞会拖住协议栈中所有其他事件。它的栈只有 4 KB,因此必须避免深层调用链和大的栈上分配缓冲区 —— 包括根据配置值确定大小的数组。
- **已投递的事件之间不会相互竞争**。由于单个任务排空 ISO 任务的各队列,两个都已投递的事件不可能并发运行;假定它们可以的推断几乎总是错误的。
- **在 NimBLE 上要注意回调所在的任务**。一次 GATT 读或写完成、或对本地服务的一次入站写,运行在 NimBLE 主机任务上而非 ISO 任务;在不同路径间共享的代码必须在两者上都正确。
- **ISO 锁排序的是访问,而非完成**。持有它保证互斥,但不保证你发起的某个操作已经完成 —— 许多传输操作在稍后的事件上才完成。
内存占用
~~~~~~~~~~~~~~~~
开源的 ESP-BLE-ISO 和 ESP-BLE-AUDIO 代码有小而确定的固定开销:
- ISO 任务使用一个 4 KB 栈和三条优先级事件队列(32 + 64 + 32 项),由单个消费者排空。
- ISO 引擎的通道、组和广播池由 ``CONFIG_BT_ISO_MAX_CHAN``、``CONFIG_BT_ISO_MAX_CIG`` 和 ``CONFIG_BT_ISO_MAX_BIG`` 确定大小;HCI 命令池只持有一个缓冲区。
- 每条连接上,NimBLE 缓存已发现的属性数据库,并为在途的 GATT 流程持有深拷贝的参数;订阅在两种主机上都用按连接的列表跟踪。
- GATT 服务属性表在初始化时分配,大小由已启用的服务决定。
由于这些池和表的大小都来自 Kconfig,开源代码的占用在构建时就固定了,除了在途的 SDU 缓冲区之外没有按流的动态增长。预构建的规范与控制器库是例外:它动态分配自己的规范和控制器状态 —— 在初始化时以及在连接和流建立时 —— 因此这部分占用并非在构建时固定。
附录 —— 文件与目录映射
--------------------------------
ESP-BLE-ISO
~~~~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 34 66
* - 目录
- 内容
* - ``api/include``
- 公共 ``esp_ble_iso_*`` 头文件。
* - ``host/common``
- 与主机无关的传输:连接、广播、扫描、GATT、HCI、ISO 粘合、L2CAP、ISO 任务和 ISO 锁;``app/`` 持有应用的 GAP 和 GATT 事件接口。
* - ``host/iso``
- ISO 引擎(CIS 和 BIG 状态机及通道 API)。
* - ``host/adapter/bluedroid``,\ |br|\ ``host/adapter/nimble``
- GAP、GATT、ISO、HCI 和(NimBLE)L2CAP 的各主机适配器。
* - ``host/utils``
- 地址、UUID、CRC、加密、定时器和缓冲区辅助函数。
ESP-BLE-AUDIO
~~~~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 34 66
* - 目录
- 内容
* - ``api/include``
- 公共 ``esp_ble_audio_*_api.h`` 头文件,每个规范和服务一个。
* - ``host/common``
- 初始化编排(``init.c``)。
* - ``host/adapter/bluedroid/profiles``,\ |br|\ ``host/adapter/nimble/profiles``
- GATT 服务适配器(PACS、ASCS、BASS、CAS、CSIS、HAS、MCS、MICS、TBS、TMAS、VCS)。
* - ``host/services/ots``
- 对象传输服务。
* - ``lib``
- 预构建的、按目标芯片的规范与控制器库。
@@ -0,0 +1,83 @@
功能支持状态
============
:link_to_translation:`en:[English]`
本页跟踪 ESP-IDF 中蓝牙 LE Audio 功能的支持状态 —— ESP-BLE-AUDIO 提供的通用音频框架(GAF)规范和服务,以及 ESP-BLE-ISO 提供的等时传输。
.. note::
下表中标为支持的蓝牙 LE Audio 功能目前为\ **预览版**\ :其 API 和行为均为暂定,可能在未来版本中变化。
下表列出了 ESP-IDF 中当前支持的蓝牙 LE Audio 规范和服务。
.. list-table::
:header-rows: 1
:widths: 28 12 60
* - 规范 / 服务
- 支持状态
- 说明
* - LE 等时通道(CIS / BIS)
- 支持
- 通过 :doc:`ESP-BLE-ISO <../../api-reference/bluetooth/esp-ble-iso>` 直接访问 ISO。
* - BAP
- 支持
- 全部六个 BAP 角色:单播客户端、单播服务器、广播源、广播接收端、广播助手、扫描委托设备。
* - PACS
- 支持
- 用于 BAP 单播服务器和广播接收端。
* - ASCS
- 支持
- 用于 BAP 单播服务器。
* - BASS
- 支持
- 用于 BAP 扫描委托设备和广播助手。
* - CAP
- 支持
- 全部三个 CAP 角色:接受端、发起端、指挥端。
* - CAS
- 支持
- 每个 CAP 接受端上的必选服务。
* - CSIP / CSIS
- 支持
- 集成员和集协调器角色。
* - VCP / VCS
- 支持
- 音量渲染器和音量控制器角色。
* - VOCS
- 支持
- 每路输出的音量偏移控制;作为可选子服务包含在 VCS 中。
* - AICS
- 支持
- 音频输入控制;作为可选子服务包含在 VCS 和 MICS 中。
* - MICP / MICS
- 支持
- 麦克风设备和麦克风控制器角色。
* - MCP / MCS
- 部分支持
- 媒体控制服务器和媒体控制客户端角色已支持。基于 OTP/OTS 的媒体对象传输暂不支持。
* - CCP / TBS
- 支持
- 通话控制服务器和通话控制客户端角色,包括 GTBS 和 TBS。
* - HAP / HAS
- 支持
- 助听器和助听器单播客户端角色,包括通过 HAS 进行预设读写。
* - TMAP / TMAS
- 支持
- 全部六个 TMAP 角色:CG、CT、UMS、UMR、BMS、BMR。
* - GMAP / GMAS
- 支持
- 全部四个 GMAP 角色:UGG、UGT、BGS、BGR。
* - PBP
- 支持
- 公共广播源和公共广播接收端角色。
* - OTP / OTS
- 不支持
- 对象传输规范/服务(MCP/MCS 用于媒体对象传输)暂不支持。
.. note::
规范和服务的定义参见 :doc:`蓝牙 LE Audio 标准 <ble-audio-introduction>`。
通用蓝牙低功耗功能支持参见 :doc:`主要功能支持状态 <../ble/ble-feature-support-status>`。
@@ -0,0 +1,25 @@
ESP-IDF 蓝牙 LE Audio
=====================
:link_to_translation:`en:[English]`
蓝牙 LE Audio 是蓝牙核心规范 5.2 中引入的音频架构。阅读本系列文档前,应注意区分以下两个概念:
- **标准**\ 指蓝牙技术联盟 (Bluetooth SIG) 定义的规范 (Profile)、服务 (Service) 与角色 (Role) 体系,其核心为构建在 LE 等时传输之上的通用音频框架 (GAF)。它与平台无关,详见 :doc:`蓝牙 LE Audio 标准 <ble-audio-introduction>`。
- **实现**\ 是在特定平台上实现该标准的软件。ESP-IDF 的实现详见 :doc:`ESP-IDF 蓝牙 LE Audio 架构 <ble-audio-architecture-overview>`。
在 ESP-IDF 中,该实现由两个组件提供:
- **ESP-BLE-ISO** 提供等时传输 —— 承载音频的连接式和广播式等时流(CIS/BIS)。参见其 :doc:`API 参考 <../../api-reference/bluetooth/esp-ble-iso>`。
- **ESP-BLE-AUDIO** 提供通用音频框架(GAF)规范和服务,构建在 ESP-BLE-ISO 之上。参见其 :doc:`API 参考 <../../api-reference/bluetooth/esp-ble-audio>`。
这两个组件均可运行于 ESP-IDF 的两个蓝牙主机协议栈 **Bluedroid** 或 **NimBLE** 之上。在可行范围内,两者的 API 与行为保持一致;当行为差异不可避免时,将在架构文档中说明,并解释原因。
如需了解各项功能的支持状态,参见 :doc:`功能支持状态 <ble-audio-feature-support-status>`。
.. toctree::
:maxdepth: 1
ble-audio-introduction
ble-audio-architecture-overview
ble-audio-feature-support-status
@@ -1,57 +1,45 @@
LE Audio 架构指南
蓝牙 LE Audio 标准
==================
:link_to_translation:`en:[English]`
本文介绍低功耗蓝牙 LE Audio 的架构:各规范(Profile)、服务(Service)的定义、它们所包含的角色,以及彼此之间的依赖关系。在使用 :doc:`ESP-BLE-AUDIO API 参考 <../../api-reference/bluetooth/esp-ble-audio>` 进行开发之前,建议先阅读本文,以便选择适合应用场景的规范组合。
本文介绍蓝牙 LE Audio 标准:各规范(Profile)、服务(Service)的定义、它们所包含的角色,以及彼此之间的依赖关系。在使用 :doc:`ESP-BLE-AUDIO API 参考 <../../api-reference/bluetooth/esp-ble-audio>` 进行开发之前,建议先阅读本文,以便选择适合应用场景的规范组合。
.. note::
本文聚焦 LE Audio **标准** 本身。关于 **ESP-IDF 的实现架构**\ (ESP-BLE-ISO 与 ESP-BLE-AUDIO 组件、任务与锁模型、各主机适配器),参见 :doc:`ESP-IDF 蓝牙 LE Audio 架构 <ble-audio-architecture-overview>`。
概述
----
低功耗蓝牙 LE Audio 是蓝牙核心规范 5.2 引入的一套规范,支持在低功耗蓝牙上传输高质量音频,具备以下核心特性:
蓝牙 LE Audio 由蓝牙核心规范 5.2 引入,支持在低功耗蓝牙上传输高质量音频,具备以下核心特性:
- **LE 等时通道(ISO)** — 控制器层的新传输机制,提供时间同步、低延迟的数据流,支持已连接(CIS)和无连接(BIS)两种模式。
- **LC3 编解码器** — 低复杂度通信编解码器(Low Complexity Communication Codec),与 SBC 相比,在更低比特率下提供更好的音频质量。
- **通用音频框架(GAF)** — 一套分层的规范和服务,标准化了音频流建立、音量控制、媒体控制、通话控制和设备协调等功能。
LE Audio 支持两种基本音频场景:
蓝牙 LE Audio 支持两种基本音频场景:
- **单播音频(Unicast Audio)** — 通过连接等时流(CIS)在两个已连接设备之间进行双向或单向音频传输。典型应用:TWS 耳机、助听器、耳麦、电话通信。
- **广播音频(Broadcast Audio,Auracast™)** — 通过广播等时流(BIS),由一个广播源向任意数量的接收端进行单向音频传输。典型应用:公共场所音频、无障碍辅助收听、群组电视收听。
架构概览
标准概览
--------
LE Audio 架构分为三个层次:
蓝牙 LE Audio 标准分为三个层次:
1. **LE 等时通道** — 传输层,提供 CIS(已连接)和 BIS(广播)流。
1. **传输层** — LE Audio 底层的蓝牙传输:LE 等时通道(CIS/BIS)承载音频数据,ACL 连接承载 GATT/ATT 规范控制,周期性广播承载广播通告。其中只有等时通道为 LE Audio 新增(详见下文)。
2. **通用音频框架(GAF)** — 核心规范套件,分为四个功能层:流控制、内容控制、渲染/采集控制和过渡/协调控制。
3. **特定用例规范** — 更高层的规范(HAP、TMAP、GMAP、PBP),针对特定使用场景选择并配置相应的 GAF 组件。
.. code-block:: none
.. figure:: ../../../_static/ble/ble-audio-gaf-zh.png
:align: center
:width: 90%
:alt: 蓝牙 LE Audio 标准分层图
┌──────────────────────────────────────────────────────────────────────┐
│ 特定用例规范 │
│ HAP TMAP GMAP PBP │
├──────────────────────────────────────────────────────────────────────┤
│ 通用音频框架(GAF) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 过渡与协调控制层 CAP + CAS │ │
│ │ CSIP + CSIS │ │
│ ├───────────────────────────────────────────────────────────────┤ │
│ │ 渲染与采集控制层 VCP(VCS、VOCS、AICS) │ │
│ │ MICP(MICS、AICS) │ │
│ ├───────────────────────────────────────────────────────────────┤ │
│ │ 内容控制层 MCP + MCS/GMCS │ │
│ │ CCP + TBS/GTBS │ │
│ ├───────────────────────────────────────────────────────────────┤ │
│ │ 流控制层 BAP(PACS、ASCS、BASS) │ │
│ └───────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ LE 等时通道(CIS / BIS) │
└──────────────────────────────────────────────────────────────────────┘
蓝牙 LE Audio 协议栈:特定用例规范构建于通用音频框架(GAF)之上。规范的控制经由 GATT/ATT 走 ACL 连接,音频数据走 LE 等时通道(CIS/BIS),广播通告走周期性广播。
GAF 各层仅依赖其下方的层。特定用例规范选择 GAF 层的子集,并在其之上添加角色特定的约束。以下各节将详细介绍每个组件。
@@ -59,7 +47,7 @@ GAF 各层仅依赖其下方的层。特定用例规范选择 GAF 层的子集
LE 等时通道
-----------
LE 等时通道是蓝牙核心规范中定义的控制器层特性,为 LE Audio 提供时间同步、低延迟的数据传输。
LE 等时通道是蓝牙核心规范中定义的控制器层特性,为蓝牙 LE Audio 提供时间同步、低延迟的数据传输。
.. list-table::
:header-rows: 1
@@ -75,13 +63,13 @@ LE 等时通道是蓝牙核心规范中定义的控制器层特性,为 LE Audi
- BIS
- 从广播方向任意数量的同步接收方进行的单向等时流,无需事先建立连接。多个 BIS 实例属于一个广播等时组(BIG)。
ESP-IDF 通过 :doc:`ESP-BLE-ISO API <../../api-reference/bluetooth/esp-ble-iso>` 提供对 CIS 和 BIS 的直接访问。使用 LE Audio 规范(BAP 及更高层)时,ISO 层由规范栈自动管理。
ESP-IDF 通过 :doc:`ESP-BLE-ISO API <../../api-reference/bluetooth/esp-ble-iso>` 提供对 CIS 和 BIS 的直接访问。使用蓝牙 LE Audio 规范(BAP 及更高层)时,ISO 层由规范栈自动管理。
通用音频框架(GAF)
-------------------
通用音频框架(GAF)是 LE Audio 的核心,定义了四个功能层,下文从下至上依次介绍。
通用音频框架(GAF)是蓝牙 LE Audio 的核心,定义了四个功能层,下文从下至上依次介绍。
流控制层
@@ -91,7 +79,7 @@ ESP-IDF 通过 :doc:`ESP-BLE-ISO API <../../api-reference/bluetooth/esp-ble-iso>
**基本音频规范(BAP)**
BAP 是所有 LE Audio 流传输的基础规范,定义了以下角色:
BAP 是所有蓝牙 LE Audio 流传输的基础规范,定义了以下角色:
- **单播客户端(Unicast Client)** — 发现远端单播服务器上的 ASE,发起编解码器配置、QoS 协商和流控制(使能、连接、开始、禁用、释放)。
- **单播服务器(Unicast Server)** — 通过 ASCS 暴露音频端点(ASE),响应客户端发起的流控制流程。
@@ -188,7 +176,7 @@ MICP 定义了 **麦克风设备(Microphone Device)**\ (暴露麦克风状
.. note::
AICS 是共用服务:VCS(作为 VCP 的一部分)和 MICS(作为 MICP 的一部分)均可包含 AICS,每者各自拥有独立的实例和句柄。
AICS 是共用服务:VCS(作为 VCP 的一部分)和 MICS(作为 MICP 的一部分)均可包含 AICS,二者各自拥有独立的实例和句柄。
过渡与协调控制层
@@ -224,14 +212,14 @@ CSIP 依赖 **协调集标识服务(CSIS)**,该服务暴露:
特定用例规范位于 GAF 之上,每个规范选择一部分 GAF 规范,定义针对特定角色的配置约束(例如编解码器参数、QoS 设置),并可添加自己的小型 GATT 服务用于角色通告。
**听力访问规范(HAP)**
**助听器访问规范(HAP)**
HAP 面向助听器设备,增加了 **听力预设(Hearing Aid Preset)** 的概念:用户可命名的音频配置(例如"室外"、"餐厅"),可在不同场景间切换。HAP 定义了:
- **助听器(Hearing Aid)** — 实现音频接收、音量控制以及(双耳助听器对)协调集成员所需的全部 GAF 角色。
- **助听器单播客户端(Hearing Aid Unicast Client)** — 发现助听器、控制预设,并管理单播音频流。
HAP 依赖 **听力访问服务(HAS)** 进行预设读写操作,同时依赖 GAF 中的 BAP、PACS、VCP、MICP 和 CSIP。
HAP 依赖 **助听器访问服务(HAS)** 进行预设读写操作,同时依赖 GAF 中的 BAP、PACS、VCP、MICP 和 CSIP。
**电话和媒体音频规范(TMAP)**
@@ -301,10 +289,72 @@ PBP 为公共广播源的元数据格式提供标准化定义,使任何兼容
PBP 完全依赖 BAP 提供底层广播传输,不定义新的 GATT 服务。
规范与服务依赖关系参考
-----------------------
规范与服务依赖关系
------------------
下图将每个缩写展开为全称,并按层次展示依赖关系——实线箭头表示依赖,虚线箭头表示可选包含的子服务。其后的表格给出各规范的精确依赖。
.. mermaid::
%%{init: {'flowchart': {'nodeSpacing': 35, 'rankSpacing': 65}}}%%
flowchart LR
HAP["HAP<br/>(Hearing Access Profile)"]
TMAP["TMAP<br/>(Telephony and Media Audio Profile)"]
GMAP["GMAP<br/>(Gaming Audio Profile)"]
PBP["PBP<br/>(Public Broadcast Profile)"]
CAP["CAP<br/>(Common Audio Profile)"]
VCP["VCP<br/>(Volume Control Profile)"]
MICP["MICP<br/>(Microphone Control Profile)"]
CSIP["CSIP<br/>(Coordinated Set Identification Profile)"]
MCP["MCP<br/>(Media Control Profile)"]
CCP["CCP<br/>(Call Control Profile)"]
BAP["BAP<br/>(Basic Audio Profile)"]
HAS["HAS<br/>(Hearing Access Service)"]
TMAS["TMAS<br/>(Telephony and Media Audio Service)"]
GMAS["GMAS<br/>(Gaming Audio Service)"]
CAS["CAS<br/>(Common Audio Service)"]
PACS["PACS<br/>(Published Audio Capabilities Service)"]
ASCS["ASCS<br/>(Audio Stream Control Service)"]
BASS["BASS<br/>(Broadcast Audio Scan Service)"]
CSIS["CSIS<br/>(Coordinated Set Identification Service)"]
VCS["VCS<br/>(Volume Control Service)"]
MICS["MICS<br/>(Microphone Control Service)"]
MCS["MCS / GMCS<br/>(Media Control Service)"]
TBS["TBS / GTBS<br/>(Telephone Bearer Service)"]
VOCS["VOCS<br/>(Volume Offset Control Service)"]
AICS["AICS<br/>(Audio Input Control Service)"]
OTS["OTS<br/>(Object Transfer Service)"]
HAP --> CAP
TMAP --> CAP & MCP & CCP
GMAP --> CAP
PBP --> BAP
CAP --> BAP & VCP & MICP & CSIP
CAP --> CAS
HAP --> HAS
TMAP --> TMAS
GMAP --> GMAS
BAP --> PACS & ASCS & BASS
VCP --> VCS
MICP --> MICS
CSIP --> CSIS
MCP --> MCS
CCP --> TBS
VCS -.-> VOCS & AICS
MICS -.-> AICS
MCS -.-> OTS
CAP ~~~ MCP
CAP ~~~ CCP
classDef uc fill:#dbe8ff,stroke:#5b8def,color:#173;
classDef coord fill:#ffe6c7,stroke:#e0922f;
classDef ctrl fill:#e3f6da,stroke:#5aa84f;
classDef svc fill:#efe3ff,stroke:#9a6fd6;
classDef opt fill:#f0f0f0,stroke:#9e9e9e,color:#555;
class HAP,TMAP,GMAP,PBP uc;
class CAP,BAP coord;
class VCP,MICP,CSIP,MCP,CCP ctrl;
class HAS,TMAS,GMAS,CAS,PACS,ASCS,BASS,CSIS,VCS,MICS,MCS,TBS svc;
class VOCS,AICS,OTS opt;
以下表格汇总了 ESP-IDF LE Audio 实现中规范与服务之间的依赖关系。
规范对服务的依赖
^^^^^^^^^^^^^^^^
@@ -414,84 +464,3 @@ PBP 完全依赖 BAP 提供底层广播传输,不定义新的 GATT 服务。
- GMAP UGT(接收端)、BAP 单播服务器、VCP 音量渲染器
* - 媒体发送端(音响)
- TMAP UMS(或广播场景的 BMS)、BAP、MCP/MCS 服务器、VCP
ESP-IDF 实现
--------------
ESP-IDF 为 LE Audio 提供两个 API 组件:
- :doc:`ESP-BLE-ISO <../../api-reference/bluetooth/esp-ble-iso>` — 直接访问 LE 等时通道(CIS/BIS),适用于需要自行管理 ISO 数据路径的应用。
- :doc:`ESP-BLE-AUDIO <../../api-reference/bluetooth/esp-ble-audio>` — 高层 LE Audio 规范和服务 API,覆盖完整 GAF 栈(BAP、CAP、VCP、MICP、CSIP、MCP、CCP、HAP、GMAP、TMAP、PBP)及编解码器支持(LC3)。
对于大多数应用,ESP-BLE-AUDIO 是正确的起点。ESP-BLE-ISO 适用于需要在规范层之下直接控制 ISO 的高级场景。
功能支持
^^^^^^^^
下表列出了 ESP-IDF 中当前支持的 LE Audio 规范和服务。
.. list-table::
:header-rows: 1
:widths: 28 12 60
* - 规范 / 服务
- 支持状态
- 说明
* - LE 等时通道(CIS / BIS)
- 支持
- 通过 :doc:`ESP-BLE-ISO <../../api-reference/bluetooth/esp-ble-iso>` 直接访问 ISO。
* - BAP
- 支持
- 全部六个 BAP 角色:单播客户端、单播服务器、广播源、广播接收端、广播助手、扫描委托端。
* - PACS
- 支持
- 用于 BAP 单播服务器和广播接收端。
* - ASCS
- 支持
- 用于 BAP 单播服务器。
* - BASS
- 支持
- 用于 BAP 扫描委托端和广播助手。
* - CAP
- 支持
- 全部三个 CAP 角色:接受端、发起端、指挥端。
* - CAS
- 支持
- 每个 CAP 接受端上的必选服务。
* - CSIP / CSIS
- 支持
- 集成员和集协调器角色。
* - VCP / VCS
- 支持
- 音量渲染器和音量控制器角色。
* - VOCS
- 支持
- 每路输出的音量偏移控制;作为可选子服务包含在 VCS 中。
* - AICS
- 支持
- 音频输入控制;作为可选子服务包含在 VCS 和 MICS 中。
* - MICP / MICS
- 支持
- 麦克风设备和麦克风控制器角色。
* - MCP / MCS
- 部分支持
- 媒体控制服务器和媒体控制客户端角色已支持。基于 OTP/OTS 的媒体对象传输暂不支持。
* - CCP / TBS
- 支持
- 通话控制服务器和通话控制客户端角色,包括 GTBS 和每承载方 TBS。
* - HAP / HAS
- 支持
- 助听器和助听器单播客户端角色,包括通过 HAS 进行预设读写。
* - TMAP / TMAS
- 支持
- 全部六个 TMAP 角色:CG、CT、UMS、UMR、BMS、BMR。
* - GMAP / GMAS
- 支持
- 全部四个 GMAP 角色:UGG、UGT、BGS、BGR。
* - PBP
- 支持
- 公共广播源和公共广播接收端角色。
* - OTP / OTS
- 不支持
- 对象传输规范/服务(MCP/MCS 用于媒体对象传输)暂不支持。