feat(ble_audio): Miscellaneous update for ISO & Audio examples

This commit is contained in:
Liu Linyan
2026-08-20 11:24:32 +08:00
parent 71ec4f2699
commit d2fee5862a
26 changed files with 2355 additions and 474 deletions
@@ -9,7 +9,9 @@
This is a raw BLE Connected Isochronous Stream (CIS) example operating directly at the ISO transport layer over either the NimBLE or Bluedroid host (selected at build time via Kconfig). It is **not** a BAP/CAP (BLE Audio profile) example — it does not implement Unicast Server/Client, ASCS, PACS, or any LC3 codec; it only exercises the underlying CIG/CIS plumbing.
The central scans for a peer advertising the name `CIS Peripheral`, opens an ACL link, optionally pairs (security level `ESP_BLE_ISO_SECURITY_NO_MITM`), creates a single-CIS CIG (10 ms SDU interval, 2M PHY, RTN 2, 120-byte SDU, sequential/unframed), connects the CIS, configures the input data path to the HCI in transparent format, and then drives a software TX scheduler that submits one SDU every 10 ms.
The central scans for a peer advertising the name `CIS Peripheral`, opens an ACL link, optionally pairs (security level `ESP_BLE_ISO_SECURITY_NO_MITM`), creates a **two-CIS** CIG (10 ms SDU interval, 2M PHY, RTN 2, 120-byte SDU, sequential/unframed), connects both CIS, configures each input data path to the HCI in transparent format, and then drives one software TX scheduler per CIS that submits one SDU every 10 ms.
Both CIS ride the same ACL connection to the same peer, which is what `esp_ble_iso_chan_connect()` expresses: it takes one ACL handle plus a channel count. Each stream is otherwise independent — its own timer, sequence numbering and TX counters — because the controller establishes them one after another, so a shared scheduler would send on a CIS that is not up yet and would report both streams under a single name.
The transmitted payload is a dummy buffer filled with the current sequence number byte — there is no real audio data, the example just demonstrates the ISO transport mechanics via the `esp_ble_iso_*` APIs.
@@ -26,7 +28,7 @@ The transmitted payload is a dummy buffer filled with the current sequence numbe
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source.
No menuconfig options — runtime defaults are baked into source. The number of CIS is the `CIS_COUNT` macro in `main/main.c`; raising it also requires `CONFIG_BT_ISO_MAX_CHAN` in `sdkconfig.defaults` to be at least that value (the host default is 1, so a second CIS is rejected without it).
### Security & Pairing
@@ -60,9 +62,9 @@ For `esp32s31`, replace the chip overlay accordingly.
2. Start passive extended scanning for a device whose Complete Local Name is `CIS Peripheral`.
3. Cancel scan and create an ACL connection (interval 80 ms, supervision 5 s) once the target is matched.
4. On ACL connect, initiate pairing because the configured security level is `ESP_BLE_ISO_SECURITY_NO_MITM`.
5. After the security change, call `esp_ble_iso_cig_create` (one CIS, 10 ms latencies and SDU interval, sequential/unframed, SCA unknown) and `esp_ble_iso_chan_connect` over the ACL handle.
6. On CIS connect, set up the input data path (HCI / transparent) and start the periodic TX scheduler.
7. The scheduler invokes `esp_ble_iso_chan_send` every 10 ms with an incrementing sequence number on a 120-byte dummy SDU.
5. After the security change, call `esp_ble_iso_cig_create` (two CIS, 10 ms latencies and SDU interval, sequential/unframed, SCA unknown) and `esp_ble_iso_chan_connect` once for both channels over the same ACL handle.
6. The controller establishes the CIS one at a time. On each CIS connect, set up that channel's input data path (HCI / transparent) and start its own periodic TX scheduler.
7. Each scheduler invokes `esp_ble_iso_chan_send` every 10 ms on its own channel, with a per-CIS incrementing sequence number on a 120-byte dummy SDU.
## Expected Log
@@ -76,17 +78,22 @@ I CIS_CEN: Connected: handle <h> role <r> peer XX:XX:XX:XX:XX:XX
I CIS_CEN: Security: handle <h> level <l> bonded <b>
```
CIS setup and streaming phase (TX log emitted every `LOG_INTERVAL_PACKETS` SDUs by the shared utility):
CIS setup and streaming phase. The two CIS come up a couple of hundred milliseconds apart, and each reports its own count (TX log emitted every `LOG_INTERVAL_PACKETS` SDUs by the shared utility):
```
I CIS_CEN: [CIS #0] Connected
I CIS_CEN: [CIS #1] Connected
I CIS_CEN: [CIS #0] TX: <count> packets
I CIS_CEN: [CIS #1] TX: <count> packets
```
Disconnect path:
There should be no `IsoSendChanNotConn` errors in the gap between the two `Connected` lines: each CIS only starts its timer once it is established, so nothing is submitted to a stream that is still connecting.
Disconnect path (per CIS, then the ACL):
```
I CIS_CEN: [CIS #0] Disconnected, reason 0x<rr>
I CIS_CEN: [CIS #1] Disconnected, reason 0x<rr>
I CIS_CEN: Disconnected: handle <h> reason 0x<rr>
```
@@ -97,6 +104,6 @@ Run [cis_peripheral](../cis_peripheral/) on a second board.
1. Flash and run `cis_peripheral` first; it begins extended advertising as `CIS Peripheral`.
2. Flash and run `cis_central` on the second board; it scans and matches that name.
3. The central creates the ACL connection and initiates pairing.
4. After the security change, the central creates the CIG and connects the CIS.
5. Both sides set up their data paths (input on central, output on peripheral).
6. The central streams 120-byte SDUs every 10 ms; the peripheral reports `RX: <count> packets` periodically.
4. After the security change, the central creates the CIG and connects both CIS over that one ACL.
5. Both sides set up a data path per CIS (input on central, output on peripheral).
6. The central streams 120-byte SDUs every 10 ms on each CIS; the peripheral reports `RX: <count> packets` per CIS periodically.
@@ -5,6 +5,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include <assert.h>
@@ -31,17 +32,37 @@
#define CIS_PHY ESP_BLE_ISO_PHY_2M
#define CIS_RTN 2
#define CIS_SDU_SIZE 120
#define CIS_COUNT 2
static bool acl_connected;
static uint16_t iso_seq_num;
static uint8_t iso_data[CIS_SDU_SIZE];
static esp_ble_iso_cig_t *out_cig;
static example_iso_tx_scheduler_t tx_scheduler;
/* Each CIS is streamed independently: it comes up at its own time and carries
* its own sequence numbering, so it gets its own timer and TX counters instead
* of sharing one scheduler (which would report both under a single name). */
struct cis_ctx {
esp_ble_iso_chan_t chan;
example_iso_tx_scheduler_t scheduler;
uint16_t seq_num;
uint8_t data[CIS_SDU_SIZE];
char name[8];
};
static void iso_chan_send(void);
static struct cis_ctx cis_ctxs[CIS_COUNT];
static void iso_chan_send(struct cis_ctx *ctx);
static struct cis_ctx *ctx_by_chan(const esp_ble_iso_chan_t *chan)
{
for (size_t i = 0; i < ARRAY_SIZE(cis_ctxs); i++) {
if (&cis_ctxs[i].chan == chan) {
return &cis_ctxs[i];
}
}
return NULL;
}
static void iso_connected_cb(esp_ble_iso_chan_t *chan)
{
@@ -49,38 +70,40 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan)
.pid = ESP_BLE_ISO_DATA_PATH_HCI,
.format = ESP_BLE_ISO_CODING_FORMAT_TRANSPARENT,
};
struct cis_ctx *ctx = ctx_by_chan(chan);
esp_err_t err;
ESP_LOGI(TAG, "[CIS #0] Connected");
ESP_LOGI(TAG, "[%s] Connected", ctx->name);
err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_INPUT, &data_path);
if (err) {
ESP_LOGE(TAG, "[CIS #0] Failed to setup data path, err %d", err);
ESP_LOGE(TAG, "[%s] Failed to setup data path, err %d", ctx->name, err);
return;
}
iso_seq_num = 0;
example_iso_tx_scheduler_reset(&tx_scheduler);
ctx->seq_num = 0;
example_iso_tx_scheduler_reset(&ctx->scheduler);
/* Note: esp timer is not accurate enough */
err = example_iso_tx_scheduler_start(&tx_scheduler, CIG_SDU_INTERVAL_US);
err = example_iso_tx_scheduler_start(&ctx->scheduler, CIG_SDU_INTERVAL_US);
if (err) {
ESP_LOGE(TAG, "[CIS #0] Scheduler start failed, err %d", err);
ESP_LOGE(TAG, "[%s] Scheduler start failed, err %d", ctx->name, err);
return;
}
iso_chan_send();
iso_chan_send(ctx);
}
static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason)
{
struct cis_ctx *ctx = ctx_by_chan(chan);
esp_err_t err;
ESP_LOGI(TAG, "[CIS #0] Disconnected, reason 0x%02x", reason);
ESP_LOGI(TAG, "[%s] Disconnected, reason 0x%02x", ctx->name, reason);
err = example_iso_tx_scheduler_stop(&tx_scheduler);
err = example_iso_tx_scheduler_stop(&ctx->scheduler);
if (err) {
ESP_LOGE(TAG, "[CIS #0] Scheduler stop failed, err %d", err);
ESP_LOGE(TAG, "[%s] Scheduler stop failed, err %d", ctx->name, err);
}
/* Per BT Core 6.0 §7.7.5: a CIS on the Central retains its handle and
@@ -91,13 +114,15 @@ static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason)
*/
err = esp_ble_iso_remove_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_INPUT);
if (err) {
ESP_LOGE(TAG, "[CIS #0] Failed to remove data path, err %d", err);
ESP_LOGE(TAG, "[%s] Failed to remove data path, err %d", ctx->name, err);
}
}
static void iso_sent_cb(esp_ble_iso_chan_t *chan, void *user_data)
{
example_iso_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "CIS #0");
struct cis_ctx *ctx = ctx_by_chan(chan);
example_iso_tx_scheduler_on_sent(&ctx->scheduler, user_data, TAG, ctx->name);
}
static esp_ble_iso_chan_ops_t iso_ops = {
@@ -117,24 +142,21 @@ static esp_ble_iso_chan_qos_t iso_qos = {
.rx = NULL,
};
static esp_ble_iso_chan_t iso_chan = {
.ops = &iso_ops,
.qos = &iso_qos,
};
static void create_cig_and_cis(uint16_t acl_handle)
{
esp_ble_iso_connect_param_t connect_param = {0};
esp_ble_iso_connect_param_t connect_param[CIS_COUNT] = {0};
esp_ble_iso_cig_param_t cig_param = {0};
esp_ble_iso_chan_t *channels[1] = {0};
esp_ble_iso_chan_t *channels[CIS_COUNT] = {0};
int err;
for (size_t i = 0; i < ARRAY_SIZE(cis_ctxs); i++) {
channels[i] = &cis_ctxs[i].chan;
}
if (out_cig) {
goto connect;
}
channels[0] = &iso_chan;
cig_param.cis_channels = channels;
cig_param.num_cis = ARRAY_SIZE(channels);
cig_param.sca = CIG_SCA;
@@ -152,37 +174,41 @@ static void create_cig_and_cis(uint16_t acl_handle)
}
connect:
connect_param.iso_chan = &iso_chan;
/* Both CIS ride the same ACL to the peer. */
for (size_t i = 0; i < ARRAY_SIZE(connect_param); i++) {
connect_param[i].iso_chan = &cis_ctxs[i].chan;
}
err = esp_ble_iso_chan_connect(&connect_param, acl_handle, 1);
err = esp_ble_iso_chan_connect(connect_param, acl_handle, ARRAY_SIZE(connect_param));
if (err) {
ESP_LOGE(TAG, "Failed to create CIS, err %d", err);
return;
}
}
static void iso_chan_send(void)
static void iso_chan_send(struct cis_ctx *ctx)
{
int err;
memset(iso_data, (uint8_t)iso_seq_num, sizeof(iso_data));
memset(ctx->data, (uint8_t)ctx->seq_num, sizeof(ctx->data));
err = esp_ble_iso_chan_send(&iso_chan,
iso_data,
sizeof(iso_data),
iso_seq_num);
err = esp_ble_iso_chan_send(&ctx->chan,
ctx->data,
sizeof(ctx->data),
ctx->seq_num);
if (err) {
ESP_LOGD(TAG, "[CIS #0] Send failed, err %d", err);
ESP_LOGD(TAG, "[%s] Send failed, err %d", ctx->name, err);
return;
}
iso_seq_num++;
ctx->seq_num++;
}
/* Each CIS has its own timer, so the scheduler only ever feeds its own channel;
* a CIS that is not established yet has no timer running and is never sent to. */
static void tx_scheduler_cb(void *arg)
{
(void)arg;
iso_chan_send();
iso_chan_send(arg);
}
static bool data_cb(uint8_t type, const uint8_t *data,
@@ -343,12 +369,21 @@ void app_main(void)
return;
}
err = example_iso_tx_scheduler_init(&tx_scheduler,
tx_scheduler_cb,
NULL);
if (err) {
ESP_LOGE(TAG, "Failed to init tx scheduler, err %d", err);
return;
/* All CIS share the callbacks and TX QoS; each keeps its own timer, fed
* with its own context so the scheduler knows which channel to send on. */
for (size_t i = 0; i < ARRAY_SIZE(cis_ctxs); i++) {
cis_ctxs[i].chan.ops = &iso_ops;
cis_ctxs[i].chan.qos = &iso_qos;
snprintf(cis_ctxs[i].name, sizeof(cis_ctxs[i].name), "CIS #%zu", i);
err = example_iso_tx_scheduler_init(&cis_ctxs[i].scheduler,
tx_scheduler_cb,
&cis_ctxs[i]);
if (err) {
ESP_LOGE(TAG, "[%s] Failed to init tx scheduler, err %d",
cis_ctxs[i].name, err);
return;
}
}
err = set_device_name();
@@ -12,6 +12,7 @@ CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_BLE_FEAT_ISO_EN=y
CONFIG_BT_ISO_CENTRAL=y
CONFIG_BT_ISO_MAX_CHAN=2
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y