作者:Steed Li, Nordic Semiconductor
在LE Audio中,Broadcast Isochronous Streams,简称BIS,即基于广播的同步音频流。与CIS不同,在BIS中,传输同步音频流的设备不知道可能有多少个设备在接收音频。设备之间没有连接,没有应答,也不需要 ACL 链路。广播音频的好处在于它允许多个设备同时监听一个广播音源,类似FM广播,其主要应用场景是在公共场所中的一群人可以佩戴耳机来共同收听音频信息。
不管是CIS还是BIS模式,都需BAP中定义的各项流程,如发送广播,接收广播音频和单播音频。接下来我们重点了解BAP中BIS广播音频的配置和建立流程,并尝试在BIS模式下,加入ACL连接和NUS服务。
BAP即Basic Audio Profile,它在GATT和GAP的基础上,定义了音频流控制的各项基本流程。BAP是实现在低功耗蓝牙上进行高效音频通信的重要组件。
广播音源设备通过BIG中的BISes来传输广播音频,与此同时,广播音源设备也会广播EA (扩展广播)PDU和PA(周期性广播) PDU。
扩展广播:
ADV_EXT_IND PDU——扩展广播PDU
AUX_ADV_IND PDU——辅助广播PDU
AUX_CHAIN_IND PDU——辅助链广播PDU(可选)
周期性广播:
AUX_SYNC_IND PDU——周期性同步广播PDU
ADV_EXT_IND PDU在37,38,39主频段进行广播,它的扩展头字段包含一个AuxPtr字段,其中包含使其能够与辅助AUX_ADV_IND PDU同步的数据。ADV_EXT_IND PDU的AuxPtr字段指向AUX_ADV_IND,即AuxPtr的值指向ADV_EXT_IND PDU广播所在的其余0~36个频段中的一个。
AUX_ADV_IND PDU的扩展头字段包含一个SyncInfo字段,其中包含使其能够与PA同步的数据。AUX_ADV_IND PDU的SyncInfo字段指向PA。
AUX_ADV_IND PDU还包含一个AdvData字段,其中包含了Broadcast Audio Announcement Service UUID和Broadcast_ID。Broadcast Audio Announcement Service UUID将PA与包含一个或多个BISes的BIG关联起来,Broadcast_ID用于帮助扫描设备确定当前EA指向的PA所指向的BIG是否它需要同步的BIG。
ADV_EXT_IND PDU和从属于它的AUX_ADV_IND PDU,都属于一个广播集合,不同的广播集合由这些PDU中的SID来指定。
周期性广播(PA)由AUX_SYNC_IND PDU和AUX_CHAIN_IND PDU(可选)组成。这里主要讨论AUX_SYNC_IND PDU。
AUX_SYNC_IND PDU的header中有一个AdvData字段,其中包含Service Data AD data type。
Service Data AD data type包 Basic Audio Announcement Service UUID,其广播内容描述一个或多个广播音频流的BASE配置,BASE用于告知扫描设备,BIG中的音频流参数,比如是哪一种Codec,Codec参数,采样频率等等。
AUX_SYNC_IND PDU扩展头字段可能携带一个ACAD字段,其中包含BIGInfo。BIGInfo数据使得可以与包含一个或多个用于传输广播音频流的BIS的BIG进行同步。因此,BIGInfo提供了接收广播音频流所需的信息。BIGInfo指向相应的BIG。
了解了以上提到的广播的PDU后,我们再来看看这些PDU的相互关系。
如上图所示,首先,在37,38,39三个主广播信道广播ADV_EXT_IND。每个BIG都需要拥有自己的广告集(advertising set)和ADV_EXT_IND。每个ADV_EXT_IND的header域都包括广告地址(AdvA),ADI(其中包括用于此组扩展广播的Set ID),以及指向AUX_ADV_IND的指针AuxPtr。
然后,在0~36个频段广播AUX_ADV_IND PDU,它们包含了描述某个特定的BIG的广告数据。AUX_ADV_IND还包括SyncInfo字段,该字段为扫描设备提供了同步AUX_SYNC_IND的信息。
最后,在辅助同步信息AUX_SYNC_IND PDU中,包含两个重要的信息。第一个是ACAD(Additional Controller Advertising Data),描述BIG及其组成的BIS的结构,也就是之前提到的BIG info。第二个是提供给接收设备信息,包含在AdvData字段中,也就是之前提到的BASE。AdvData字段包含基本音频公告服务UUID和BASE,其中包含BIG中音频流的详细定义,包括编解码器配置和元数据,以用户可读的格式描述音频流的用例和内容。
接下来我们通过抓包来更直观地观察上述广播的过程:
首先任意挑选一个ADV_EXT_IND PDU包,通过AuxPtr找到属于它的AUX_ADV_IND PDU所在的频段6和偏移位置2.07ms。
然后通过AUX_ADV_IND PDU中的Header中的Sync info字段信息,找到属于它的AUX_SYNC_IND PDU,这里是偏移大约31.83ms。
再来看这个AUX_SYNC_IND包中,对BIG的描述信息的解读,主要在BIG info中:
BIG Offset——对应的BIG的偏移位置,这里大约为960us。
ISO interval——ISO间隔,也就是BIG的间隔,10ms。
Sub Interval —— 354us, Sub event的间隔。
BIS Spacing —— 相邻两组BISes的起始点的间距,这里大约为1.062ms。
Num BIS —— 2,BIS的组数2,即有两组BISes。
NSE —— 3,在一个BIG里面,每组BIS的sub event的数量。
Max PDU —— 40,BIS PDU最大字节数。
SDU Interval —— 10ms,可等同理解为ISO Interval。
Max SDU Size —— 40,BIG中每个SDU最大字节数,可等同理解为Max PDU。
BN,PTO,IRC控制着BIG事件中有哪些数据被传输。
BAP中定义了广播设备的状态机,并包含以下几个流程:
The Broadcast Audio Stream configuration procedure —— 广播音频流配置
The Broadcast Audio Stream establishment procedure —— 广播音频流建立
The Broadcast Audio Stream disable procedure —— 广播音频流关闭
The Broadcast Audio Stream Metadata update procedure —— 广播音频流元数据更新
The Broadcast Audio Stream release procedure —— 广播音频流释放
The Broadcast Audio Stream reconfiguration procedure —— 广播音频流重配置
下面我们结合nRF Connect SDK中LE Audio的应用代码来解析一个广播音频流的建立过程:
配置BASE,将其添加到周期性广播(PA)的广播数据(adv_data)中。
int bt_bap_broadcast_source_get_base(struct bt_bap_broadcast_source *source,
struct net_buf_simple *base_buf)
{
CHECKIF(source == NULL) {
LOG_DBG("source is NULL");
return -EINVAL;
}
CHECKIF(base_buf == NULL) {
LOG_DBG("base_buf is NULL");
return -EINVAL;
}
if (!encode_base(source, base_buf)) {
LOG_DBG("base_buf %p with size %u not large enough", base_buf, base_buf->size);
return -EMSGSIZE;
}
return 0;
}
/* Setup periodic advertising data */
ret = bt_bap_broadcast_source_get_base(broadcast_source, &base_buf);
if (ret) {
LOG_ERR("Failed to get encoded BASE: %d", ret);
return ret;
}
per_ad.type = BT_DATA_SVC_DATA16;
per_ad.data_len = base_buf.len;
per_ad.data = base_buf.data;
ret = bt_le_per_adv_set_data(adv, &per_ad, 1);
if (ret) {
LOG_ERR("Failed to set periodic advertising data: %d", ret);
return ret;
}
建立BIG
/* Create BIG */
param.num_bis = bis_count;
param.bis_channels = bis;
param.framing = source->qos->framing;
param.packing = source->packing;
param.interval = source->qos->interval;
param.latency = source->qos->latency;
param.encryption = source->encryption;
if (param.encryption) {
(void)memcpy(param.bcode, source->broadcast_code,
sizeof(param.bcode));
}
err = bt_iso_big_create(adv, ¶m, &source->big);
if (err != 0) {
LOG_DBG("Failed to create BIG: %d", err);
return err;
}
设置扩展广播和周期性广播
int le_audio_enable(le_audio_receive_cb recv_cb, le_audio_timestamp_cb timestmp_cb)
{
int ret;
ARG_UNUSED(recv_cb);
LOG_INF("Starting broadcast gateway %s", CONFIG_BT_AUDIO_BROADCAST_NAME);
ret = initialize(timestmp_cb);
if (ret) {
LOG_ERR("Failed to initialize");
return ret;
}
service_init();
advertising_start();
/* Start extended advertising */
ret = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT);
if (ret) {
LOG_ERR("Failed to start extended advertising: %d", ret);
return ret;
}
/* Enable Periodic Advertising */
ret = bt_le_per_adv_start(adv);
if (ret) {
LOG_ERR("Failed to enable periodic advertising: %d", ret);
return ret;
}
/*
ret = bt_le_ext_adv_start(adv_conn, BT_LE_EXT_ADV_START_DEFAULT);
if (ret) {
LOG_ERR("Failed to start connectable extended advertising: %d", ret);
return ret;
}
*/
LOG_DBG("Starting broadcast source");
ret = bt_bap_broadcast_source_start(broadcast_source, adv);
if (ret) {
return ret;
}
LOG_DBG("LE Audio enabled");
return 0;
}
建立广播音频流
当扩展广播和周期性广播开始运行的时候,就要开始建立一个广播音频流。首先进入同步广播模式(Broadcast Isochronous Broadcasting Mode ),为发送BISes PDU做准备,然后开启Broadcast Isochronous Synchronizability Mode,在周期性广播(PA)中发送BIGinfo,通知所有扫描设备其所携带的BIG和BIS的信息。最后通过the LE Setup ISO Data Path HCI command来设置广播音频流的路径。
if (IS_ENABLED(CONFIG_BT_ISO_BROADCASTER) &&
iso->iso.info.type == BT_ISO_CHAN_TYPE_BROADCASTER && in_path) {
dir = BT_HCI_DATAPATH_DIR_HOST_TO_CTLR;
err = hci_le_setup_iso_data_path(iso, dir, in_path);
if (err != 0) {
LOG_DBG("Failed to set broadcaster data path: %d", err);
}
return err;
} else if (IS_ENABLED(CONFIG_BT_ISO_SYNC_RECEIVER) &&
iso->iso.info.type == BT_ISO_CHAN_TYPE_SYNC_RECEIVER &&
out_path) {
dir = BT_HCI_DATAPATH_DIR_CTLR_TO_HOST;
err = hci_le_setup_iso_data_path(iso, dir, out_path);
if (err != 0) {
LOG_DBG("Failed to set sync receiver data path: %d", err);
}
}
接下来我们尝试在BIS模式下的gateway添加NUS服务。
应用场景:在广播数据的过程中,需要将数据上传到BLE主机,比如老师在教学过程中,将教学信息上传到蓝牙网关。
实现:参考peripheral_uart的例子。首先,建立一个可连接的扩展广播,并初始化广播参数和广播包内容。
static int adv_create(void)
{
int ret;
/* Broadcast Audio Streaming Endpoint advertising data */
NET_BUF_SIMPLE_DEFINE(ad_buf, BT_UUID_SIZE_16 + BT_AUDIO_BROADCAST_ID_SIZE);
/* Buffer for Public Broadcast Announcement */
NET_BUF_SIMPLE_DEFINE(base_buf, 128);
NET_BUF_SIMPLE_DEFINE(pba_buf, BT_UUID_SIZE_16 + 2);
struct bt_data ext_ad[4];
uint8_t pba_features = 0;
struct bt_data ext_ad[3];
struct bt_data per_ad;
uint32_t broadcast_id = 0;
/* Create a non-connectable non-scannable advertising set */
ret = bt_le_ext_adv_create(LE_AUDIO_EXTENDED_ADV_NAME, NULL, &adv);
if (ret) {
LOG_ERR("Unable to create extended advertising set: %d", ret);
return ret;
}
/* Create a connectable scannable advertising set */
ret = bt_le_ext_adv_create(LE_AUDIO_EXTENDED_ADV_CONN_NAME, NULL, &adv_conn);
if (ret) {
LOG_ERR("Unable to create connectable extended advertising set: %d", ret);
return ret;
}
}
static const struct bt_data ad_peer[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_NUS_VAL)
};
开启广播:
int le_audio_enable(le_audio_receive_cb recv_cb, le_audio_timestamp_cb timestmp_cb)
{
int ret;
ARG_UNUSED(recv_cb);
LOG_INF("Starting broadcast gateway %s", CONFIG_BT_AUDIO_BROADCAST_NAME);
ret = initialize(timestmp_cb);
if (ret) {
LOG_ERR("Failed to initialize");
return ret;
}
service_init();
advertising_start();
}
然后,初始化NUS服务:
int service_init(void)
{
int err = 0;
err = uart_init();
if (err) {
LOG_ERR("Failed to initialize UART driver (err: %d)", err);
return 0;
}
LOG_INF("Bluetooth initialized");
k_sem_give(&ble_init_ok);
/*if (IS_ENABLED(CONFIG_SETTINGS)) {
settings_load();
}*/
err = bt_nus_init(&nus_cb);
if (err) {
LOG_ERR("Failed to initialize UART service (err: %d)", err);
return 0;
}
return 0;
}
注册蓝牙连接回调事件:
static struct bt_conn_cb conn_callbacks = {
.connected = connected_cb,
.disconnected = disconnected_cb,
// .security_changed = security_changed_cb,
};
在connected回调事件中,请求连接参数更新,这一步很重要,因为LE audio的controller中 广播音频流的优先级是最高的,在广播音频流传输的过程中很容易打断ACL连接,之前已经知道iso interval为10ms,因此这里把ACL的连接间隔调整到100~200ms。
static void connected_cb(struct bt_conn *conn, uint8_t err)
{
int ret;
char addr[BT_ADDR_LE_STR_LEN];
uint16_t conn_handle;
enum ble_hci_vs_tx_power conn_tx_pwr;
struct bt_le_conn_param param = {
.interval_min = 80,
.interval_max = 160,
.latency = 2,
.timeout = 400,
};
LOG_INF("Request conn param min=%d max=%d", param.interval_min, param.interval_max);
bt_conn_le_param_update(conn, ¶m);
if (err) {
default_conn = NULL;
return;
}
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INF("Connected: %s", addr);
ret = bt_hci_get_conn_handle(conn, &conn_handle);
if (ret) {
LOG_ERR("Unable to get conn handle");
} else {
conn_tx_pwr = CONFIG_NRF_21540_MAIN_DBM;
conn_tx_pwr = CONFIG_BLE_CONN_TX_POWER_DBM;
ret = ble_hci_vsc_conn_tx_pwr_set(conn_handle, conn_tx_pwr);
if (ret) {
LOG_ERR("Failed to set TX power for conn");
} else {
LOG_DBG("TX power set to %d dBm for connection %p", conn_tx_pwr, (void *)conn);
}
}
default_conn = bt_conn_ref(conn);
}
将代码烧录到Nordic audio DK,打开手机的nRF Connect app,可以看到有一个可连接广播和一个不可连接广播,连接到可连接广播,即可发现NUS服务并与手机进行数据通信。
观看网络研讨会:LE Audio BIS 模式流程解析
中文官网:www.nordicsemi.cn
英文官网:www.nordicsemi.com
微信公众号:nordicsemi
https://devzone.nordicsemi.com
北京分公司: +86 010 8438 2767
上海分公司: +86 21 6330 0620
深圳分公司: +86 755 8322 0147
sales.cn@nordicsemi.no
按下方提示星标 Nordic🌟
以免错过半导体行业深度好文👇
点击“阅读原文” 进入Nordic半导体中文官网