跳转至

泰凌 LE Audio CIS


快速使用指南

概述

LE Audio 是下⼀代蓝牙音频技术,使通过低功耗蓝牙传输音频成为可能。与经典 (BR/EDR) 音频相比,它支持新的用例并显着降低功耗。蓝牙LE Audio旨在降低功耗、减小延迟、缩小传输带宽、并最终提升性能。LE Audio 和 Classic Audio 标准将继续共存,并具有两者都支持的功能。LE Audio 将具有更广泛的功能集、更低的功耗和更好的可感知音频质量。LE Audio 不仅具有更好的音频质量和更长的播放时间,而且还引入了新的功能和机会。

CIS(Connected Isochronous Stream)是LE Audio中的一个用例,另外一个用例为BIS(Broadcast Isochronous Stream),BIS将在另一个handbook中描述,这里仅针对CIS进行描述。

SDK特性

  • 支持Bluetooth LE 5.3;

  • 支持LE Audio传输,48KHz/16 bits下行(LC3),16KHz/16 bits上行(LC3),音频延时≤ 40 ms;

  • 支持多个采样率:8kHz、16kHz、24kHz、32kHz、44.1kHz、48kHz;

  • 支持TWS耳机;

  • 支持LC3编解码;

  • 支持自适应跳频;

  • 支持headset。

SDK应用领域

  • TWS耳机

  • Headset

下载工具

工具:烧录调试工具

固件.bin:http://192.168.48.36/sdk_app/ble/telink_b91m_ble_audio_sdk(Telink内部链接)

Demo演示

参考Telink BLE Audio SDK用户指南(Telink内部链接)。

SDK使用及采用的架构

软件准备

开发软件安装及使用

登录泰凌的官方文档网站,在软件文档 页面选择工具介绍可以看到下载选项和如何激活并导入工程。本节将介绍该SDK的导入与使用方法。

(1) 打开IDE;鼠标右键点击Project Explorer,弹出菜单中点击Import,如下图所示:

点击Import

(2) 选择 “Existing Projects into Workspace”,点击Next,如下图所示:

点击Step 1 & Step 2

(3) 点击Browse...,选中SDK目录,可以看到Projects窗口中弹出BLE_AUDIO_SDK,选择对应的芯片,B91对应的是TLSR951x的芯片,此时再点Finish即可完成SDK导入,如图所示:

选择对应芯片

编译顺序

Audio_unicast_client表示的是Client工程,audio_unicast_server表示的是Server工程,目前Client的程序编译需要先编译ble_audio_bootloader,再编译Client。Server因为暂时没加bootloader,所以直接编译Server就可以。如下图所示:

选择显示的选项

编译通过会在B91m_ble_sdk\build\B91\audio_unicast_client\output 里看到下图红框所示的bin文件,Server工程同理。但是因为Client加了bootloader,所以必须使用“boot”开头的bin文件,如下图所示:

选择显示的选项

软件组织结构

在IDE导入工程后,显示的文件夹组织结构如下图所示。顶层文件夹有8个,分别是algorithm、 application、boot、common、drivers、proj_lib、stack、vendor。

文件夹组织结构

  • algorithm: 提供⼀些通用的算法,如aes_ccm、LC3编解码器。大多数算法对应的 C 文件被封装在库文件中,只留对应的头文件。

  • application: 提供一些通用的应用处理程序,如print、keyboard等。

  • boot: 提供芯片的 software bootloader,即 MCU 上电启动或 Deepsleep 唤醒后的汇编处理过程,为后面C语言程序的运行搭建好环境。

  • common: 提供⼀些通用的跨平台的处理函数,如内存处理函数、字符串处理函数等。

  • drivers: 提供与 MCU 紧密相关的硬件设置和外设驱动程序,如clock、flash、I2C、USB、GPIO、UART等。

  • proj_lib: 存放 SDK 运行所必需的库(如 libB91_driver_i2.a)。BLE协议栈、RF驱动、PM驱动等文件,被封装在库文件里,用户无法看到源文件。

  • stack: 存放 BLE 协议栈相关的文件。

  • vendor: 用于存放用户应用层代码。

Client/Server任务架构

在SDK中,RF和system中断是BLE协议栈使用的,不对用户开放,用户无需关心具体内容。下面的中断都是目前SDK中所用到的,其他没有用到的中断,用户可根据需要来使用。

Time0中断

Time0中断主要用来控制音频数据的播放时间。

USB中断

USB中断里面包含了USB数据的输入和输出,包括Speak、MIC、UDB和HID。HID作为控制测试使用。

UART中断

UART中断包括了UART0和UART1 。目前UART0/1都可以用来打印系统log,也可以作为shell组件来使用。Shell组件也作为产测的测试使用。

app_le_enhanced_connection_complete_event_handle

连接完成事件(Event),用户在这里可以对连接完成后做一些判定性操作,比如:对连接Client端的MAC地址进行识别,连接完成后Server端的LED变化、提示音等。

app_disconnect_event_handle

断开ACL事件,用户可以在这里面添加断开ACL之后的逻辑,比如灯效,以及判断断开 ACL 连接的原因,是否需要重新打开Scan/ADV。

app_controller_event_callback

BLE协议栈Controller的事件回调。

app_host_event_callback

BLE协议栈Host层的事件回调。

Client/Server任务架构

连接和断开

LE Audio SDK-Unicast场景的连接主要包括ACL(异步)连接和CIS(同步)。ACL连接是CIS连接存在的基础,要建立CIS连接必须先建立ACL连接。断连时,如果断开CIS,则ACL连接不受影响,如果断开ACL连接,如果存在CIS连接,则CIS连接会一同断开。为了应对多设备场景服务发现的问题(如TWS场景一个Central对两个Peripheral),LE Audio引入了CSIP(协调集)协议。通过CSIP协议,Central发现了协调集内一个Peripheral之后即可发现该协调集中所有的Peripheral设备。另外,建立CIS连接的时候涉及到Client和Server的音频参数配置,以及数据流状态的变化。

连接

连接初始化

Client

设置MAC地址:

void blc_initMacAddress(int flash_addr, u8 *mac_public, u8 *mac_random_static)

设置Client ACL连接的交流间隔:

blc_ll_setAclCentralBaseConnectionInterval(CONN_INTERVAL_20MS);

设置发射功率:

blc_ll_setDefaultTxPowerLevel(RF_POWER_P9dBm);

设置加密等级:

blc_smp_setSecurityLevel_central(Unauthenticated_Pairing_with_Encryption);
blc_smp_setSecurityParameters_central(Bondable_Mode, 0, LE_Secure_Connection, 0, 0, IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
blc_smp_setEcdhDebugMode_central(debug_mode);

选择场景,目前可选择的场景有TWS和Headset,本文档以TWS作为示例。

#define APP_AUDIO_SCENE       APP_SCENE_TWS

配置上下行buffer size以及sample和byte大小。

#define APP_AUDIO_INPUT_BUFFER_SIZE                  2048
#define APP_AUDIO_INPUT_FRAME_SAMPLE_MAX             480
#define APP_AUDIO_INPUT_FRAME_ENCODE_BYTES_MAX       155
#define APP_AUDIO_OUTPUT_BUFFER_SIZE                 2048
#define APP_AUDIO_OUTPUT_FRAME_SAMPLE_MAX            160
#define APP_AUDIO_OUTPUT_FRAME_ENCODE_BYTES_MAX      40

选择上下行场景和采样率。

typedef enum{BLC_AUDIO_1_SVR_1_SINK_1_CHN_1_SRC_N_CHN_N_CISES_1_STREAMS_1, BLC_AUDIO_2_SVR_1_SINK_N_CHN_N_SRC_1_CHN_1_CISES_1_STREAMS_1,
.......
BLC_AUDIO_11II_SVR_2_SINK_2_CHN_1_SRC_2_CHN_1_CISES_2_STREAMS_4, BLC_AUDIO_STD_AUDIO_CONFIGURATIONS_E_MAX,} std_unicast_aud_cfg_enum;

音频能力支持初始化,需要用户选择。

//BAP_v1.0.1 page 25, Table 3.5: Unicast Server audio capability support requirements
//BAP_v1.0.1 page 33, Table 3.11: Unicast Client audio capability support requirements
const std_codec_settings_t codecSettings[16] = {
{
BLC_AUDIO_SUPP_FREQ_FLAG_8000, BLC_AUDIO_FREQ_CFG_8000, BLC_AUDIO_SUPP_DURATION_FLAG_7_5,BLC_AUDIO_DURATION_CFG_7_5, 26
},
......
{
BLC_AUDIO_SUPP_FREQ_FLAG_48000,BLC_AUDIO_FREQ_CFG_48000, BLC_AUDIO_SUPP_DURATION_FLAG_10, BLC_AUDIO_DURATION_CFG_10,  155
},
};

设置扩展广播的参数。

ble_sts_t blc_ll_setExtScanParam (own_addr_type_t  ownAddrType, scan_fp_type_t scan_fp,scan_phy_t scan_phys,scan_type_t  scanType_0,    scan_inter_t  scanInter_0, scan_wind_t scanWindow_0,scan_type_t scanType_1, scan_inter_t scanInter_1, scan_wind_t scanWindow_1);

使能广播。

ble_sts_t blc_ll_setExtScanEnable (scan_en_t  extScan_en, dupe_fltr_en_t filter_duplicate, scan_durn_t duration, scan_period_t period);
服务器

设置MAC地址。

void blc_initMacAddress(int flash_addr, u8 *mac_public, u8 *mac_random_static)

设置发射功率。

blc_ll_setDefaultTxPowerLevel(RF_POWER_P9dBm);

设置加密等级。

blc_smp_setSecurityLevel_central(Unauthenticated_Pairing_with_Encryption);
blc_smp_setSecurityParameters_central(Bondable_Mode, 0, LE_Secure_Connection, 0, 0, IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
blc_smp_setEcdhDebugMode_central(debug_mode);

BAP单播服务器初始化。

blc_audio_registerBapUnicastServer(&unicastSvrParam);

参数说明。

typedef struct{
u8 sinkPacNum;                  //number of Sink PAC records
const blc_audio_pacParam_t* sinkPac;    //Sink PAC
u32 sinkAudioLocations;         //Sink Audio Location, BLC_AUDIO_LOCATION_FLAG_FL
u8 sourcePacNum;                    //number of Source PAC records
const blc_audio_pacParam_t* sourcePac;  //Source PAC
u32 sourceAudioLocations;       //Source Audio Location, BLC_AUDIO_LOCATION_FLAG_FL
u16 availableSinkContexts;      //Available Sink Contexts, BLC_AUDIO_CONTEXT_TYPE_UNSPECIFIED
u16 availableSourceContexts;    //Available Source Contexts, BLC_AUDIO_CONTEXT_TYPE_UNSPECIFIED
u16 supportedSinkContexts;      //Supported Sink Contexts, BLC_AUDIO_CONTEXT_TYPE_UNSPECIFIED
u16 supportedSourceContexts;    //Supported Sink Contexts, BLC_AUDIO_CONTEXT_TYPE_UNSPECIFIED
} blc_pacss_regParam_t;
  • sinkPacNum: Sink PAC的数量。

  • sinkPac: Sink PAC的能力,音频接收能力属性,当设备支持音频接收时才需要定义。

  • sinkAudioLocations: Sink支持音频位置。

  • sourcePacNum: Source PAC的数量,音频发送能力属性,当设备支持音频发送时才需要定义。

  • sourcePac: source PAC的能力。

  • sourceAudioLocations: source支持音频位置。

  • availableSinkContexts: 可用的Sink音频内容。

  • availableSourceContexts: 可用的 source 音频内容。

  • supportedSinkContexts: 支持的 Sink 音频内容。

  • supportedSourceContexts: 支持的 source 音频内容。

默认的配置,用户可修改,目前没有使用默认配置,而是使用了const blc_pacss_regParam_t pacsParam。

const blc_pacss_regParam_t defaultPacsParam = {
    .sinkPacNum = 1,
    .sinkPac = defaultPac,
    .sinkAudioLocations = BLC_AUDIO_LOCATION_FLAG_FL,
    .sourcePacNum = 1,
    .sourcePac = defaultPac,
    .sourceAudioLocations = BLC_AUDIO_LOCATION_FLAG_FL,
    .availableSinkContexts = BLC_AUDIO_CONTEXT_TYPE_UNSPECIFIED|BLC_AUDIO_CONTEXT_TYPE_CONVERSATIONAL|BLC_AUDIO_CONTEXT_TYPE_MEDIA,
    .availableSourceContexts = BLC_AUDIO_CONTEXT_TYPE_UNSPECIFIED|BLC_AUDIO_CONTEXT_TYPE_CONVERSATIONAL|BLC_AUDIO_CONTEXT_TYPE_MEDIA,
    .supportedSinkContexts = BLC_AUDIO_CONTEXT_TYPE_UNSPECIFIED|BLC_AUDIO_CONTEXT_TYPE_CONVERSATIONAL|BLC_AUDIO_CONTEXT_TYPE_MEDIA,
    .supportedSourceContexts = BLC_AUDIO_CONTEXT_TYPE_UNSPECIFIED|BLC_AUDIO_CONTEXT_TYPE_CONVERSATIONAL|BLC_AUDIO_CONTEXT_TYPE_MEDIA,
};

定义Audio的位置,常见的是左右声道。

enum{
BLC_AUDIO_LOCATION_FLAG_FL                = BIT(0),  // Front Left
BLC_AUDIO_LOCATION_FLAG_FR                = BIT(1),  // Front Right
.......
BLC_AUDIO_LOCATION_FLAG_RS                = BIT(27), // Right Surround
BLC_AUDIO_LOCATION_FLAG_RFU               = BITS(28,29,30,31) // bit28 ~ bit29
};
#define BLC_AUDIO_CHANNEL_ALLOCATION_RFU(param) (param&BLC_AUDIO_LOCATION_FLAG_RFU)

设定Server的音频能力。

const blc_audio_pacParam_t sinkPac[] = {
{
LC3_CAP_16_2(BLC_AUDIO_CHANNEL_COUNTS_1, 1),
METADATA_CONTEXTS(BLC_AUDIO_CONTEXT_TYPE_CONVERSATIONAL|BLC_AUDIO_CONTEXT_TYPE_MEDIA),
},
.......
{
LC3_CAP_48_2(BLC_AUDIO_CHANNEL_COUNTS_1, 1),                     
METADATA_CONTEXTS(BLC_AUDIO_CONTEXT_TYPE_CONVERSATIONAL|BLC_AUDIO_CONTEXT_TYPE_MEDIA),
},
};

初始化CSIP。

blc_audio_registerCSISControlServer(&csipSetMemberParam); 

设置SIRK值(Set Identity Resolving Key)。

#define CSISS_DEFAULT_LOCK_TIMEOUT              60      //60s
#define CSISS_DEFAULT_PLAIN_SIRK                {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0f, 0x10}        //must 16byte
const blc_csiss_regParam_t defaultCsipSetMemberParam =
{
    .setSize = 2,
    .setRank = 1,
    .lockedTimeout = CSISS_DEFAULT_LOCK_TIMEOUT,
    .SIRK_type = 1,
    .SIRK = CSISS_DEFAULT_PLAIN_SIRK,
};

选择场景,目前可选择的场景有TWS和headset,本文档以TWS作为示例。

#define APP_AUDIO_SCENE           APP_SCENE_TWS

扩展广播参数设定。

blc_ll_setExtAdvParam(ADV_HANDLE0, ADV_EVT_PROP_EXTENDED_CONNECTABLE_UNDIRECTED, ADV_INTERVAL_30MS, ADV_INTERVAL_30MS, BLT_ENABLE_ADV_ALL, OWN_ADDRESS_PUBLIC, BLE_ADDR_PUBLIC, NULL, ADV_FP_NONE, TX_POWER_3dBm,BLE_PHY_1M, 0,  BLE_PHY_1M, ADV_SID_0, 0);

初始化广播类型,用户也可以在扩展广播中增加自己需要的定制广播类型。

blc_adv_ltv_t *adv_ltvs[] = {
    (blc_adv_ltv_t *) &advDefFlags,
    (blc_adv_ltv_t *) &advDefCompleteName,
    (blc_adv_ltv_t *) &advDefAppearance,
    (blc_adv_ltv_t *) &advIncompleteList,
    (blc_adv_ltv_t *) &advCsisRsi,
    (blc_adv_ltv_t *) &capTargetAnnouncement,
    (blc_adv_ltv_t *) &bapTargetDefAnnouncement,
    (blc_adv_ltv_t *) &advTmapRole,
};
u8 advData[255];
u8 adv_ext_len = blc_adv_buildAdvData(adv_ltvs,ARRAY_SIZE(adv_ltvs),advData);

使能扩展广播。

ble_sts_t   blc_ll_setExtAdvEnable(adv_en_t enable, u8 adv_handle, u16 duration, u8 max_extAdvEvt);

连接流程

Client: central_pairing_enable = 1 && 足够近的距离 && blc_ll_setExtScanEnable(BLC_SCAN_ENABLE, DUP_FILTER_DISABLE, SCAN_DURATION_CONTINUOUS, SCAN_WINDOW_CONTINUOUS);

Server: 足够近距离 && blc_ll_setExtAdvEnable(BLC_ADV_ENABLE, ADV_HANDLE0, 0, 0)

(1) 只要Client和Server的状态按照上述设定了之后,就可以连接上,同时Client会保存Server的配对信息,包括MAC等。

(2) 如果关机再开机等有过断连操作的,Client会自动先查看在flash保存的Server的信息,如果能找到的话,那么这时候Client和Server走的是回连的过程。回连过程Client不需要重新对Server进行服务发现,所以比配对过程要快很多。

(3) 如果用户手动把保存的信息给清理掉的话,那么下一次连接走的会是配对过程。

Client处设定的连接条件。

(1) user_manual_pairing =central_pairing_enable && (temp_rssi > -56);
(2) if(blc_csis_resolveRSI(appCtrl.acl_csis_sirk, adv_data->data))
{       
rsi_auto_connect = (1 &&(temp_rssi > -56));
}
(3) if(central_auto_connect || user_manual_pairing || rsi_auto_connect)

三个条件各有用处,第一个central_auto_connect在SDK没有使用到,暂时不做介绍。

第二个条件是用户手动打开配对,且距离要在一定的范围内。

第三个是Client主动在保存信息里面搜索保存的SIRK值,保证能正确连上在协调集里的Server。

Client处设定的过滤条件如下:

if(ServiceUUID == SERVICE_UUID_AUDIO_STREAM_CONTROL )
{
app_adv_announcement_t *p = (app_adv_announcement_t *)&adv_data->data[2];
if( p->available_audio_context > 0x100) 
{
    announcement_filter = 1;
}
if(p->announcement_type == BLC_AUDIO_TARGETED_ANNOUNCEMENT && p->available_audio_co ntext | (BLC_AUDIO_CONTEXT_TYPE_CONVERSATIONAL|BLC_AUDIO_CONTEXT_TYPE_MEDIA))
{
    announcement_filter = 1;
}
}

拿到的信息会进行一次判断,目前只做了一些Telink设备和某款Server的适配,其他的适配可根据Server的具体情况来适配。

Client和Server建立ACL的流程这里不做介绍,简要介绍CIS流程,具体可以参考蓝牙core5.3 page 2979 & 2980。

CIS流程

断开

断连流程

在LE Audio中,连接的话先创建ACL连接,然后再创建CIS连接,CIS音频流必须在ACL之后,且依赖于ACL的连接,如果仅断开CIS的连接,对ACL没有影响,但是如果断开ACL的连接,那么CIS的连接也会断开。

ACL断连导致断连:

ACL断连

主动终止同步连接流(Client和Server都可以主动发起):

设备A终止已建立的CIS连接

断开连接API:

blc_ll_disconnect (u16 connHandle, u8 reason);
blc_ll_cis_disconnect(u16 cisHandle, u8 reason);

断连原因

typedef enum {
    BLE_SUCCESS = 0,
//  HCI Status, See the Core_v5.0(Vol 2/Part D/1.3 "list of Error Codes") for more information)
    HCI_ERR_UNKNOWN_HCI_CMD                                        = 0x01,
    HCI_ERR_UNKNOWN_CONN_ID                                        = 0x02,
    HCI_ERR_HW_FAILURE                                             = 0x03,
    HCI_ERR_PAGE_TIMEOUT                                           = 0x04,
    HCI_ERR_AUTH_FAILURE                                           = 0x05,
    HCI_ERR_PIN_KEY_MISSING                                        = 0x06,
......
    //LE Audio Server
    LE_AUDIO_SERVER_INVALID_SERVICE                                = 0xF0,
    LE_AUDIO_SERVER_INVALID_HANDLE,
} ble_sts_t;

若用户对某些原因不明确,可联系Telink 技术支持。

超距断连 & 关机断连:若用户触发这两个断连,通常上报原因为 HCI_ERR_CONN_TIMEOUT。

清除配对记录:清除配对记录有多种方法,这里只介绍一种方法。

for(u8 i=0;i<appCtrl.acl_max_num;i++){
    if(0 != conn_dev_list[i].conn_state){
        if(blc_ll_disconnect(conn_dev_list[i].conn_handle, HCI_ERR_REMOTE_USER_TERM_CONN) == BLE_SUCCESS)
        {
            dev_char_info_t* dev_char_info = dev_char_info_search_by_connhandle(conn_dev_list[i].conn_handle);//connHandle has marked on on central_unpair_enable
            #if (ACL_CENTRAL_SMP_ENABLE)
            blc_smp_deleteBondingPeripheralInfo_by_PeerMacAddress(dev_char_info->peer_adrType, dev_char_info->peer_addr);
            #endif
            }
        }
    } 

事件上报

应用层上报

参考app_audio.c和app_audio.h。在SDK中,LE Controller和Host有部分事件是被Audio Profile底层接管的,对于这些被接管的事件是否上报依赖于LE Controller(对应API: blc_hci_le_setEventMask_cmd)和Host(对应API: blc_gap_setEventMask)的事件掩码是否开启。

Profile层事件回调是所有音频流处理的入口。用户需要通过Profile层事件回调获取ASE的状态,并根据ASE状态配置执行APP层相应操作。目前SDK示例了下面的事件上报。

static const app_audio_evtCb_t unicastCb[] = {
    /* Event for controller or Host */
    {PRF_EVTID_ACL_CONNECT              , (void*)app_event_acl_connect},
    {PRF_EVTID_ACL_DISCONNECT           , (void*)app_event_acl_disconnect},
    {AUDIO_EVT_CIS_CONNECT              , (void*)app_event_cis_connect},
    {AUDIO_EVT_CIS_DISCONNECT           , (void*)app_event_cis_disconnect},
    {AUDIO_EVT_CIS_REQUEST              , (void*)app_event_cis_request},
    {PRF_EVTID_SMP_SECURITY_DONE        , (void*)app_event_security_done},
    /* Event for Client SDP */
    {PRF_EVTID_CLIENT_SDP_FOUND         , (void*)app_event_audio_sdp_found},
    {PRF_EVTID_CLIENT_SDP_FAIL          , (void*)app_event_audio_sdp_not_found},
    {PRF_EVTID_CLIENT_ALL_SDP_OVER      , (void*)app_event_audio_sdp_over},
    /* Event for BAP Unicast Server */
    {AUDIO_EVT_BAPUS_CODEC_CONFIGURED   , (void*)app_ep_codec_configured},
    {AUDIO_EVT_BAPUS_QOS_CONFIGURED     , (void*)app_ep_qos_configured},
    {AUDIO_EVT_BAPUS_ENABLING           , (void*)app_ep_enabling},
    {AUDIO_EVT_BAPUS_RECEIVE_STREAMING  , (void*)app_ep_receive_streaming},
    {AUDIO_EVT_BAPUS_SEND_STREAMING     , (void*)app_ep_send_streaming},
    {AUDIO_EVT_BAPUS_DISABLING          , (void*)app_ep_disabling},
    {AUDIO_EVT_BAPUS_RELEASING          , (void*)app_ep_releasing},
};

(1) ACL Connnect & ACL Disconnect

app_event_acl_connect
app_event_acl_disconnect

用户通过该事件,获取当前 ACL 状态。并可以根据需要,在该事件回调中开启/关闭扩展广播。

(2) CIS Connnect & CIS Disconnect & CIS REQUEST

app_event_cis_connect
app_event_cis_disconnect
app_event_cis_request

用户通过该类事件,获取 CIS 连接状态。CIS 相关操作由 Profile 层接管,该事件的设立是为了让用户更好的理解音频流建立过程,用户无需在该事件回调中执行任何操作。

(3) SDP_FOUND & SDP_FAIL & SDP_OVER

app_event_audio_sdp_found
app_event_audio_sdp_not_found
app_event_audio_sdp_over

Profile 注册后,第一次 ACL 连接时,Unicast Server 会对 Unicast Client 进行注册 Profile 的 Service Discovery。如果服务发现成功,Profile 层会启动AUDIO_EVT_CLIENT_SDP_FOUND回调。如果服务发现失败,会启动AUDIO_EVT_CLIENT_SDP_FAIL回调。用户可以清晰的得知自己注册 Profile 在 Unicast Server 端的存在情况。每个 Profile 事件对应的 ID 参考audio_svc_role_enum。

Host事件上报

int app_host_event_callback (u32 h, u8 *para, int n)
{
    u8 event = h & 0xFF;
    switch(event)
    {
        case GAP_EVT_SMP_PAIRING_BEGIN:
        {
        }
        break;
......
}
......
}

Host层其他的事件的上报在Host事件回调里面处理,没有太多需要用户关注的事件。

Controller事件上报

int app_controller_event_callback (u32 h, u8 *p, int n)
{
    if (h &HCI_FLAG_EVENT_BT_STD)       //Controller HCI event
    {
        u8 evtCode = h & 0xff;
        //------------ disconnect -------------------------------------
        if(evtCode == HCI_EVT_DISCONNECTION_COMPLETE)  //connection terminate
        {
            app_disconnect_event_handle(p);
        }
......
}
......
}

Controller层其他的事件的上报在Controller事件回调里面处理,主要有连接完成事件、广播事件、连接更新和CIS连接完成事件。这些事件SDK已有做相应处理,可参考相应的章节。

音频数据的流动

在音频流的配置中,Unicast Server将自己的音频能力通过 PAC 暴露给Unicast Client,Unicast Client获取Unicast Server的音频能力后结合自身偏好选择一组音频参数配置Unicast Server, 启动音频流。在这个过程中,Unicast Server是被动的,需要根据Unicast Client配置的参数,实时更新自己的CODEC配置。Unicast Server通过 ASE 状态变更为 CODEC Configured 后获取音频端点参数,获取所有音频端点参数后(QoS configured状态上报即可认为所有CODEC参数配置完成),Unicast Server 在本地配置LC3和CODEC。配置完之后即可进行数据的流动。除非音频采集部分或者MIC采集部分没有数据可以收集,亦或是用户主动断开音频的链路,那么音频流会一直存在。

数据流状态机

ASCS: 音频流控制服务,公开设备的音频流端点,并允许客户端发现、配置、建立和控制音频流端点以及与之相关的单播音频流。

ASCS服务主要由Audio Stream Endpoint(简称ASE)承载,ASE分两种:

  • Source ASE: 音频流出的方向。

  • Sink ASE: 音频流入的方向。

数据流状态机

Source ASE 有 7 个状态,分别为:

  • IDLE: 待机状态,初始化默认处于此状态,可以通过 Release 操作从 Releasing 状态进入。

  • CODEC Configured: 音频参数,如采样率、位宽、位长等配置完成。可以通过 Config CODEC 操作从 IDLE 状态进入,通过 Release 操作从 Releasing 状态进入,通过 Config CODEC 操作从 Qos Configured 状态进入。

  • QoS Configured: 连接参数,如 PHY、Retransmit Num、Max Transport Latency 等配置完成,可以通过Config QoS操作从 CODEC Configured 状态进入,通过 Receive Stop Ready 操作从 Disabling 状态进入。

  • Enabling: 使能态,Client 与 Server 之间的 CIS 开始建立或者已经建立完成。只能通过 Enable 操作从QoS Configured状态进入。在 Enabling 状态下可以更新 metadata。

  • Streaming: 流状态,音频流建立完成,音频数据可以在 Client 与 Server 之间传递。此状态只能通过Receive Start Ready操作从 Enabling 状态进入。在 Streaming 状态下可以更新metadata。

  • Disabling: 失效态,音频流停止,该状态是一个中间态,可以通过 Receive Start Stop 操作退回到 QoS Configured 状态。需要注意: 仅 Source ASE 具备该状态。可以通过 Disable 操作从Enabling或者Streaming状态进入。

  • Releasing: 释放态,音频资源释放,该状态是一个中间态,可以从除IDLE状态的任何一个状态进入,可以通过 Released 操作 Release 到 CODEC Configured 态(缓存,减少从释放音频流到再次建立音频流所用的时间) 或者 IDLE 态(完全释放)。

Sink ASE 共有 6 个状态,其所有状态和操作均可参考 Source ASE。相比于Source ASE,Sink ASE 缺少Disabling状态,所以从 Enabling 或 Streaming 状态 Disable 时,将直接退回到 QoS Configured 状态。

下行数据流

数据流动框图

数据流动如下图所示,分为Application和BLE Stack,BLE Stack的发送接收都不需要用户关注,SDK已经做好适配封装,用户只需要关注Application,且Application的内容SDK会全部做好接口,用户只需要简单设置即可使用。

Application & BLE Stack 数据流动框图

Application & BLE Stack 数据流动框图

音频数据采集

下行USB数据采集:

_attribute_ram_code_ void  app_usb_irq_proc (void)
{
    if (usbhw_get_eps_irq()&FLD_USB_EDP6_IRQ)
    {
        ......
        for (unsigned int i=0; i<len; i++)
        {
            usbData[i] = reg_usb_ep6_dat;
        }
        tlk_buffer_write(usbData,len,TLK_BUFFER_1)
        ......
    }
}

在初始化的时候,已经定义好数据的间隔、大小和端点,所以在函数里直接获取端点的数据即可,目前SDK是将获取的数据放入专属buffer里。

音频算法处理

void tlka_ppm_asrc_16_bit_init(void *st, TLKA_PPM_ASRC_CHANNEL channel, int ppm);
void tlka_ppm_asrc_24_bit_init(void *st, TLKA_PPM_ASRC_CHANNEL channel, unsigned int buffer_len, int ppm_data_len_max);

目前SDK里,尚未集成ASRC,其他的算法也没有加上,更多的介绍请参考算法部分。

LC3编解码

单个的 LC3 编解码器只支持一路音频通道,所以立体声音频需要两个 LC3 编解码器才能工作。在SDK中,LC3编解码器相关的API代码文件: algorithm/audio_alg/lc3/lc3.h。使用 LC3 编解码器,需要先重新定义宏,确定 LC3 编码通道数量、LC3 解码通道数量。如 CIS Client Demo 中,既需要将采集到的立体声音频,编码后发送给两个Server,同时需要将两个Server上报的数据解码为立体声音频。所以需要LC3编码器数量等于2,LC3解码器数量等于2。

#define LC3_ENCODE_CHANNEL_COUNT 2
#define LC3_DECODE_CHANNEL_COUNT 2

其他的场景,用户根据需求,自行配置编解码器数量。

注意

  • 需要注意 RAM size(请参考附录)。
lc3enc_encode_init

调用该 API 可以初始化 LC3 编码器,针对了解 LC3 编码器的用户使用。

int lc3enc_encode_init(u8 index, u32 nSamplerate, u32 nBitrate, u16 nMs_mode);

输入参数:

  • index: LC3 编码器索引,范围为 [0,LC3_ENCODE_CHANNAL_COUNT-1]。

  • nSamplerate: 音频的采样率,单位Hz。8kHz对应8000Hz。

  • nBitrate: 编码后的比特率。

  • nMs_mode: 音频采样周期。1表示7.5ms,0表示10ms。

  • 返回值: 0表示初始化成功。其他值表示失败。

lc3enc_encode_init_bap

调用该 API 可以初始化 LC3 编码器,输入是 BAP 规范的参数。

int lc3enc_encode_init_bap(u8 index, u8 samplingFreq, u8 frameDuration, u16 perCodecFrame);

输入参数:

  • index: LC3 编码器索引,范围为 [0,LC3_ENCODE_CHANNAL_COUNT-1]。

  • samplingFreq: 音频的采样率。

  • frameDuration: 音频采样周期。

  • perCodecFrame: 编码后的长度。

  • 返回值: 0表示初始化成功。其他值表示失败。

lc3enc_encode_pkt

调用该 API 可以真正编码音频数据,需要先初始化编码模块。

int lc3enc_encode_pkt(u8 index, u8* rawData, u8* encData);

输入参数:

  • index: LC3 编码器索引,范围为 [0,LC3_ENCODE_CHANNAL_COUNT-1]。

  • rawData: 原始数据指针。

  • encData: 编码后的数据指针。

  • 返回值: 0 表示初始化成功。其他值表示失败。

lc3dec_decode_init

调用该 API 可以初始化 LC3 解码器,针对了解 LC3 解码器的用户使用。

int lc3dec_decode_init(u8 index, u32 nSamplerate, u32 nBitrate, u16 nMs_mode);

输入参数:

  • index: LC3 解码器索引,范围为 [0,LC3_DECODE_CHANNAL_COUNT-1]。

  • nSamplerate: 音频的采样率,单位Hz。8kHz对应8000Hz。

  • nBitrate: 编码后的比特率。

  • nMs_mode: 音频采样周期。1表示7.5ms、0表示10ms。

返回值: 0表示初始化成功。其他值表示失败。

lc3dec_decode_init_bap

调用该 API 可以初始化 LC3 解码器,输入是 BAP 规范的参数

int lc3dec_decode_init_bap(u8 index, u8 samplingFreq, u8 frameDuration, u16 perCodecFrame);

输入参数:

  • index: LC3 解码器索引,范围为 [O,LC3_DECODE_CHANNAL_COUNT-1]。

  • samplingFreq: 音频的采样率。

  • frameDuration: 音频采样周期。

  • perCodecFrame: 编码后的长度。

  • 返回值: 0 表示初始化成功。其他值表示失败。

lc3dec_decode_pkt

调用该 API 可以真正解码出音频数据,需要先初始化解码模块。

int lc3dec_decode_pkt(u8 index, u8* encData, u16 encDataLen, u8* rawData);

输入参数:

  • index: LC3 解码器索引,范围为 [0,LC3_DECODE_CHANNAL_COUNT-1].

  • encData: 编码数据指针。

  • encDataLen: 解码数据长度

  • rawData: 解码后的音频数据指针。

  • 返回值: 0 表示初始化成功。其他值表示失败。

lc3dec_set_parameter

调用该 API 可以修改 LC3 解码器参数,目前仅用于 PLC 补包功能。需要在解码数据前调用。

int lc3dec_set_parameter(u8 index, LC3_PARAMETER para, u32* val);

输入参数:

  • index: LC3 解码器索引,范围为 [0,LC3_DECODE_CHANNAL_COUNT-1]。

  • para: 固定参数 LC3_PARA_BEC_DETECT。

  • val: 1表示没有这块数据,需要解码器补包。

  • 返回值: 0 表示初始化成功。其他值表示失败。

编码示例
static void app_audio_receive_process(void)
{
sdu_packet_t* pPkt = blc_bapuc_sduPacketPop(appCtrl.aclParam[i].acl_handle, 0);
......
if(pPkt->iso_sdu_len!=codecSettings[APP_AUDIO_CODEC_OUTPUT_PARAMETER_PREFER].frameOctets)
LC3DEC_Error ret_lc3 = lc3dec_set_parameter(i, LC3_PARA_BEC_DETECT, &detect);
......
ret_lc3 = lc3dec_decode_pkt(i,pPkt->data,pPkt->iso_sdu_len,(u8*)pRaw.buffer);
......
}
解码示例
LC3ENC_Error ret_lc3 = lc3enc_encode_pkt(i,(u8*)audio_pcm,audio_enc[i]);

音频播放

void app_codec_receive_process(void)   

Client 与 Server 端的音频流完全建立之后(如果下行存在),Client 可以开始音频接收、解码、播放流程。

LE Audio 协议规定,Unicast Server 端每一包音频数据(SDU)在接收时都附带一个Timestamp(参考 core_V5.4,Vol6,Part G 3.2,SDU synchronization reference),是这一包数据对应的渲染时间(未来的时间)。该时间加Client与Server端确定的演示延时(Presentation Delay)就是这一包对应的 Render Point。Server需要保证接收到的每一包音频数据在对应的 Render Point 播放出来。

为了确保 Client 端收到的每一个 SDU 在固定时间点(Render Point)播放,Server 引入了 Timer0 中断。音频数据包附带的 Render Point 作为 Timer0 的 capture 时间点。

为了兼容性和 APP 层代码的高效简洁,Telink LE Audio SDK在音频数据播放时引入了动态内存分配机制。具体的应用原理为:Server端在收到音频数据后,经过解码得到原始的 PCM 音频数据,但此时还没有到达 Render Point,此时Server端使用 malloc 将这一笔音频数据存到链表中。经过连续操作,Server 会得到 Render Point 有序的链表。链表中 Render Point 由近到远排列,链表头(空)的下一个节点需要在最近的时间点播放,链表尾需要在最远的时间点播放。Render Point 有序链表配合 Timer 中断,即可使音频数据源源不断的播放。

Unicast Server 端 TWS 场景和 Headset 场景对应的 CODEC 输出流程有所不同。

  • TWS 场景对应的 CODEC 输出: Server 端接 Client 端传输的单声道音频数据解码并播放。

  • Headset 场景对应的 CODEC 输出: Server 端接收 Client 端传输的双声道音频数据(多路复用,一个 SDU包含两个声道的数据),解码后合并成双声道数据播放。

上行数据流

上行数据的框图与下行数据基本一致,数据采集部分为MIC数据。SDK在初始化时默认初始化上行,上行是打开的状态,用户可以在需要用到MIC数据的时候再打开MIC数据的解码和填入buffer。

音频数据采集

void app_codec_send_process(void)

Client 与 Server 端的音频流完全建立之后(如果音频上行存在),Client可以开始音频数据的采集、编码、发送流程。

音频算法处理

目前SDK里,只有PLC补包,其他的算法没有加上,更多的介绍参考算法部分。

Client 端接收数据时,Controller 会按照固定 Interval 均匀的上报固定长度的 SDU。如果 SDU 的长度异常,就可以判定底层的 Controller 有丢包情况,此时 APP 层可启动 LC3 PLC 补包流程。PLC 流程能够增加音频链路的抗干扰性,在不影响音频体验的情况下增加音频传输距离。

LC3编解码

同下行。

音频播放

同下行。

自定义数据发送与接收

/**
 * @brief      This function is used to send ISO data.
 * @param[in]  cisHandle or bisHandle
 * @param[in]  pData  point to data to send
 * @param[in]  len  the length to send
 * @return      Status - 0x00:  succeeded;
 *                       other:  failed
 */
ble_sts_t blc_iso_sendData(u16 handle, u8 *pData, u16 len);

用于 CIS 端发送 ISO data,调用该 API 后,数据会被先缓存到 SDU FIFO,然后根据当前ISO流参数拆分为 PDU 数据格式,在合适的时间发送到对方设备。

/**
 * @brief       Send ATT Value Notify.
 * @param[in]   connHandle   - connection handle.
 * @param[in]   attHandle    - ATT handle.
 * @param[in]   p            - Pointer point to application data buffer.
 * @param[in]   len          - the length of data.
 * @return      none.
 */
ble_sts_t   blc_gatt_pushHandleValueNotify  (u16 connHandle, u16 attHandle, u8 *p, int len);

/**
 * @brief       This function serves to push audio data.
 * @param[in]   aclHandle - acl connect handle
 * @param[in]   epId      - endpoint index
 * @param[in]   pPkt      - packet
 * @param[in]   pktL      - packet data length
 * @return      none.
 */
audio_error_enum blc_bapus_sduPacketPush(u16 aclHandle,u8 aseID, u8* pPkt, u16 pktLen);

媒体/电话控制

MCS 和通用 MCS (GMCS) 公开提供媒体播放状态和控制的特征,用于描述媒体播放器信息,包括媒体播放器类别、图标和当前渲染目标、当前曲目和相关曲目信息、下一曲目、播放速度、当前组(多个轨道的组)、当前组内轨道的播放顺序。媒体播放器的媒体状态: 向其提供音频的关联音频流,媒体播放器上搜索操作的结果。MCP 定义了角色和规程,用于控制实现 MCS 或 GMCS 的远程设备。

TBS 和通用 TBS (GTBS) 为可以拨打和接听电话的设备上的承载提供电话呼叫控制接口和状态。该服务公开了承载信息,包括服务提供商、技术类型(3G,4G,VoIP等)、信号强度等,每个活动呼叫的状态以及用于控制该呼叫的方法,例如活动、本地保持、远程保持、警报等,也可以通过相应地控制每个呼叫状态的状态来实现多路呼叫(例如,将一个保持为保留状态,将另一个保持活动状态,或者将两个都处于活动状态以进行三向呼叫)。CCP 定义了用于控制实现 TBS 或 GTBS 的远程设备的角色和规程。

媒体控制

媒体初始化

由于在CAP角色中,MCP 和CCP都是Server控制Client,所以在初始化的时候,需要在Server端注册Client角色。Client 端注册Server角色。

Server和Client注册服务:

blc_audio_registerMediaControlClient(NULL); //MCP Media Control Client init
blc_audio_registerMediaControlServer(NULL);

Server端参数初始化,默认,用户不需要修改。如果用户想修改参数,请联系Telink的技术支持。

u8 defaultGmcsMediaPlayerName[] = { 'T', 'e', 'l', 'i', 'n', 'k', '-', 'M', 'e', 'd', 'i', 'a' };
const blc_mcps_regParam_t defaultMcpsParam = {
    .gmcsParam = {
        .mediaPlayerName = defaultGmcsMediaPlayerName,
        .mediaPlayerNameLen = sizeof(defaultGmcsMediaPlayerName),
        .mediaPlayerIconObjectIdPresent = false,
        .currentTrackSegmentsObjectIdPresent = false,
        .currentTrackObjectIdPresent = false,
        .nextTrackObjectIdPresent = false,
        .parentGroupObjectIdPresent = false,
        .currentGroupObjectIdPresent = false,
        .mediaPlayerIconUrl = NULL,
        .mediaPlayerIconUrlLen = 0,
        .trackTitle = NULL,
        .trackTitleLen = 0,
        .trackDuration = 0,
        .trackPosition = 0,
        .mediaState = GMCS_MEDIA_STATE_PAUSED,
        .CCID = 0x01,
        .mediaControlPointOpcodesSupported = 0xFFFF,
        .playbackSpeed = 0,
        .seekingSpeed = 0,
        .playingOrdersSupported = 0xFF,
        .playingOrder = 1,
    }
};

USB HID描述符初始化:

const uint8_t hid_report_descriptor[] = {
        0x05, 0x0c,  // USAGE_PAGE (Consumer Devices)
        0x09, 0x01,  // USAGE (Consumer Control)
        0xa1, 0x01,  // COLLECTION (Application)
        0x85, USB_HID_KB_MEDIA,  //global, report ID 0x03
        0x15, 0x00,  //Logical Minimum (0)
        0x25, 0x01,  //Logical Maximum (1)
        0x09, 0xE9,  //Usage (Volume Increment)
        0x09, 0xEA,  //Usage (Volume Decrement)
        0x09, 0xE2,  //Usage (Mute)
        0x09, 0xCD,  //Usage (Play/Pause)
        0x09, 0xB5,  //Usage (Scan Next Track)
        0x09, 0xB6,  //Usage (Scan Previous Track)
.......
};

媒体使用

Client端事件上报如下示例,用户可在函数里添加需要的逻辑,目前SDK已加默认逻辑,具体可参考SDK代码。

static void app_event_media_control(u16 aclHandle, blc_mcss_mediaControlPointEvt_t *pEvt)
{
    blc_mcss_mediaControlPointEvt_t *op = pEvt;
    switch(op->opcode)
    {
        case BLC_MCS_OPCODE_PLAY:
            break;
        case BLC_MCS_OPCODE_PAUSE:
            break;
        case BLC_MCS_OPCODE_NEXT_TRACK:
            break;
        case BLC_MCS_OPCODE_FIRST_TRACK:
            break;
        default:
            break;
    }
}

Server端回调示例,用户可在函数里添加需要的逻辑,目前SDK已加默认逻辑,具体可参考SDK代码。

/**
 * @brief       Media event callback in APP layer,used to inform user about 'media state' and 'media information'
 */
void app_media_event_callback(u16 connHandle, int evtID, u8 *pData, u16 dataLen)
{
    switch(evtID)
    {
        case AUDIO_EVT_MCSC_MEDIA_PLAYER_NAME:
        {
        }
        break;
......
        default:
        break;
    }
}

开始播放:

/**
 * @brief       This function serves to control the remote media,if success the media will convert to playing state.
 */
void app_audio_media_play(u16 connHandle);

暂停播放:

/**
 * @brief       This function serves to control the remote media,if success the media will convert to pause state.
 */
void app_audio_media_pause(u16 connHandle);

上一曲:

/**
 * @brief       This function serves to control the remote media,if success the media will convert current track to previous track.
 */
void app_audio_media_previous_track(u16 connHandle);

下一曲:

/**
 * @brief       This function serves to control the remote media,if success the media will convert current track to next track.
 */
void app_audio_media_next_track(u16 connHandle);

更多的操作参考app_audio_media.c 和mcs.h。

USB HID控制主机端媒体状态:

void my_app_cmd_rcvd(u8 cmd, u8 data)
{
......
case HID_AUDIO_PLAY_PAUSE:
    {
        usbaudio_report_consumer_key(0x08);
        hid_release_tick = clock_time()|1;
        key_release_timing = 10000;
    }
break;
......
}

媒体事件

GMCS Client Event ID详解:

typedef enum{
    AUDIO_EVT_GMCSC_START = AUDIO_EVT_TYPE_GMCSC,
    AUDIO_EVT_MCSC_MEDIA_PLAYER_NAME,         
    AUDIO_EVT_MCSC_MEDIA_TRACK_CHANGED,         
    AUDIO_EVT_MCSC_MEDIA_TRACK_TITLE,            
    AUDIO_EVT_MCSC_MEDIA_TRACK_DURATION,       
    AUDIO_EVT_MCSC_MEDIA_TRACK_POSITION,        
    AUDIO_EVT_MCSC_MEDIA_PLAYBACK_SPEED,       
    AUDIO_EVT_MCSC_MEDIA_SEEKING_SPEED,          
    AUDIO_EVT_MCSC_MEDIA_CURRENT_TRACK_OBJECT_ID, 
    AUDIO_EVT_MCSC_MEDIA_NEXT_TRACK_OBJECT_ID,    
    AUDIO_EVT_MCSC_MEDIA_PARENT_GROUP_OBJECT_ID,  
    AUDIO_EVT_MCSC_MEDIA_CURRENT_GROUP_OBJECT_ID, 
    AUDIO_EVT_MCSC_MEDIA_PLAYING_ORDER,           
    AUDIO_EVT_MCSC_MEDIA_STATE,                  
    AUDIO_EVT_MCSC_MEDIA_CTRL_RESULT,          
    AUDIO_EVT_MCSC_MEDIA_CTRL_OPCODE_SUPPORT,     
    AUDIO_EVT_MCSC_SEARCH_CTRL_RESULT,         
    AUDIO_EVT_MCSC_SEARCH_RESULT_OBJECT_ID,       
} audio_gmcsc_evt_enum;

(1) AUDIO EVT_MCSC MEDIA PLAYER NAME

当MCS Server端的媒体播放器名称发生变化时,应该通知MCS Client端。MCS Client端收到通知后通过该事件上报给 APP 层。事件附带的参数信息结构体如下:

typedef struct{
    u16 connHandle;
    u8  mediaNameLen;
    u8  mediaName[50];
} blc_mcsc_mediaPlayerNameEvt_t;

(2) AUDIO_EVT_MCSC_MEDIA_TRACK_CHANGED

当MCS Server端的媒体播放器当前播放曲目发生变化时,应该通知MCS Client端。MCS Client端收到通知后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u16 connHandle;
} blc_mcsc_mediaTrackChangedEvt_t;

(3) AUDIO_EVT_MCSC_MEDIA_PLAYING_ORDER

当MCS Server端的媒体播放器播放顺序发生变化时,应该通知 MCS Client 端。MCS Client端收到通知后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u16 connHandle;
    int order;  //blc_mcs_playingOrder_enum
}blc_mcsc_mediaPlayingOrderEvt_t;

参数信息:

  • connHandle: ACL连接句柄;

  • order: 媒体播放器播放顺序,具体内容请参考结构体blc_mcs_playingOrder_enum。

typedef enum{
    BLC_MCS_PLAYING_ORDER_SINGLE_ONCE = 0x01,
    BLC_MCS_PLAYING_ORDER_SINGLE_REPEAT,
    BLC_MCS_PLAYING_ORDER_IN_ORDER_ONCE,
    BLC_MCS_PLAYING_ORDER_IN_ORDER_REPEAT,
    BLC_MCS_PLAYING_ORDER_OLDEST_ONCE,
    BLC_MCS_PLAYING_ORDER_OLDEST_REPEAT,
    BLC_MCS_PLAYING_ORDER_NEWEST_ONCE,
    BLC_MCS_PLAYING_ORDER_NEWEST_REPEAT,
    BLC_MCS_PLAYING_ORDER_SHUFFLE_ONCE,
    BLC_MCS_PLAYING_ORDER_SHUFFLE_REPEAT,
    BLC_MCS_PLAYING_ORDER_RFU = 0XFF,
}blc_mcs_playingOrder_enum;

(4) AUDIO_EVT_MCSC_MEDIA_STATE

当 MCS Server 端的媒体播放器播放状态发生变化时,应该通知 MCS Client 端。MCS Client端收到通知后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u16 connHandle;
    int state;  //blc_mcs_mediaState_enum
}blc_mcsc_mediaStateEvt_t;
typedef enum{
    GMCS_MEDIA_STATE_INACTIVE           = 0x00,
    GMCS_MEDIA_STATE_PLAYING          = 0x01,
    GMCS_MEDIA_STATE_PAUSED             = 0x02,
    GMCS_MEDIA_STATE_SEEKING            = 0x03,
    GMCS_MEDIA_STATE_RFU                = 0xff,
}blc_mcs_mediaState_enum;

更多的解释请参考mcs.h。

电话控制

电话初始化

由于在CAP角色中,CCP是Server控制Client,所以在初始化的时候,需要在Server端注册Client角色。Client端注册Server角色。

Server和Client注册服务。

blc_audio_registerCallControlServer(NULL);
blc_audio_registerCallControlClient(NULL); 

默认参数,用户不需要修改,如修改,请联系Telink技术支持。

const u8 defaultGtbsBearerProviderName[] = { 'D', 'e', 'f', 'a', 'u', 'l', 't', ' ', 'p', 'r', 'o', 'v', 'i', 'd', 'e', 'r' };
const u8 defaultGtbsBearerUci[] = { 'u', 'n', '0', '0', '0' };
const u8 defaultGtbsUriScheme[] = { 't', 'e', 'l' };
const blc_tbss_uri_scheme_t defaultGtbsURISchemes[] = {
    {
        .uri = defaultGtbsUriScheme,
        .uriLen = sizeof(defaultGtbsUriScheme),
    },
};

const blc_ccps_regParam_t defaultCppsParam = {
    .gtbsParam = {
        .bearerProviderName = defaultGtbsBearerProviderName,
        .bearerProviderNameLen = sizeof(defaultGtbsBearerProviderName),
        .bearerUci = defaultGtbsBearerUci,
        .bearerUciLen = sizeof(defaultGtbsBearerUci),
        .bearerTechnology = GTBS_TECHNOLOGY_3G,
        .bearerUriSchemeList = (blc_tbss_uri_scheme_t*)defaultGtbsURISchemes,
        .bearerUriSchemeListLen = ARRAY_SIZE(defaultGtbsURISchemes),
        .signalStrength = GTBS_SIGNAL_STRENGTH_UNAVAILABLE,
        .CCID = 0,
        .statusFlags.statusFlags = 0,
    }
};

电话使用

Client端事件上报如下示例,用户可在函数里添加需要的逻辑,目前SDK已加默认逻辑,具体可参考SDK代码。

void app_audio_telephone_event_callback(u16 connHandle, int evtID, u8 *pData, u16 dataLen)
{
    switch (evtID) {
    case AUDIO_EVT_GTBSS_BEARER_SIGNAL_STRENGTH_REPORTING_INTERVAL:
    {
    }
break;
......
    case AUDIO_EVT_GTBSS_CALL_CONTROL_POINT_LOCAL_RETRIEVE:
    {
    }
    break;
    }
}

Server端回调示例,用户可在函数里添加需要的逻辑,目前SDK已加默认逻辑,具体可参考SDK代码。

void app_call_event_callback(u16 connHandle, int evtID, u8 *pData, u16 dataLen)
{
    switch(evtID)
    {
        case AUDIO_EVT_GTBS_BEARER_PROVIDER_NAME:
        {
        }
        break;
        ......
        case AUDIO_EVT_GTBS_CCP_NTF_RESULT_CODE:
        {
        }
        break;
        default:
        break;
    }
}

接听电话:

/**
 * @brief       This function serves to excute the accept operaiton,if success the call will convert to active state.
 */
void app_audio_call_accept(u16 connHandle,u8 callIndex)

挂断/拒接电话:

/**
 * @brief       This function serves to terminate the call.
 */
void app_audio_call_terminate(u16 connHandle,u8 callIndex)

更多请参考app_audio_call.c。

电话事件

Client端电话事件详解:

typedef enum{
    AUDIO_EVT_GTBSC_START = AUDIO_EVT_TYPE_GTBSC,
    AUDIO_EVT_GTBS_BEARER_PROVIDER_NAME,         
    AUDIO_EVT_GTBS_BEARER_TECHNOLOGY,           
    AUDIO_EVT_GTBS_BEARER_URI_SCHEMES_SUPP_LIST,
    AUDIO_EVT_GTBS_BEARER_SIGNAL_STRENGTH,       
    AUDIO_EVT_GTBS_BEARER_LIST_CURRENT_CALL,    
    AUDIO_EVT_GTBS_STATUS_FLAGS,               
    AUDIO_EVT_GTBS_INCOMING_CALL_TGT_URI,       
    AUDIO_EVT_GTBS_CALL_STATE,              
    AUDIO_EVT_GTBS_TERM_REASON,             
    AUDIO_EVT_GTBS_INCOMING_CALL,             
    AUDIO_EVT_GTBS_CALL_FRIENDLY_NAME,      
    AUDIO_EVT_GTBS_CCP_NTF_RESULT_CODE,        
} audio_gtbsc_evt_enum;

(1) AUDIO_EVT_GTBS_BEARER_PROVIDER_NAME

当GTBS Server端的通话服务商名称(Bearer Provider Name)发生变化时,需要通知 GTBS Client 端,GTBS Client 端收到后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u8 nameLen;
    u8 providerName[30];//e.g. "CMCC"
} blc_gtbsc_bearerProviderName_t;

(2) AUDIO_EVT_GTBS_BEARER_TECHNOLOGY

当GTBS Server端的通话服务采用的技术(Bearer Technology)发生变化时,需要通知 GTBS Client 端,GTBS Client 端收到后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    int technology; //blc_gtbs_technology_enum
} blc_gtbsc_technology_t;
typedef enum
{
    GTBS_TECHNOLOGY_3G                         =0x01,
    GTBS_TECHNOLOGY_4G                         =0x02,
    GTBS_TECHNOLOGY_LTE                        =0x03,
    GTBS_TECHNOLOGY_WIFI                       =0x04,
    GTBS_TECHNOLOGY_5G                         =0x05,
    GTBS_TECHNOLOGY_GSM                        =0x06,
    GTBS_TECHNOLOGY_CDMA                       =0x07,
    GTBS_TECHNOLOGY_2G                         =0x08,
    GTBS_TECHNOLOGY_WCDMA                      =0x09,
    GTBS_TECHNOLOGY_IP                         =0x0a,
}blc_gtbs_technology_enum;

(3) AUDIO_EVT_GTBS_BEARER_URI_SCHEMES_SUPP_LIST

当GTBS Server端的通话服务商支持的 URI 方案(Bearer URI Schemes Supported List)发生变化时,需要通知GTBS Client端,GTBS Client端收到后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u8 suppLen;
    u8 uriSchemeSuppList[30];//e.g.: "tel,sip,skype"
} blc_gtbsc_uriSchemeSuppList_t;

参数信息:

  • suppLen: 支持的列表长度

  • uriSchemeSuppList: 通话服务商支持的 URI 方案,例如支持通话和skype,列表为"tel,skype”。最长 30 个字节,超过 30 个字节将只显示 30 个字节。URI 方案由国际互联网工程任务组(IETF)负责分配,发布和维护。

(4) AUDIO_EVT_GTBS_BEARER_SIGNAL_STRENGTH

当GTBS Server端的通讯服务信号强度(Bearer Signal Strength)发生变化时,需要通知 GTBS Client 端,GTBS Client端收到后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u8 signalStrength;
} blc_gtbsc_signalStrength_t;

参数信息:

  • signalStrength: 通讯服务讯号强度。取值范围为0-100,以及255。0 代表无服务;100 代表信号最强;255代表信号强度不可用。

(5) AUDIO_EVT_GTBS_BEARER_LIST_CURRENT_CALL

当GTBS Server端的当前通话服务列表(Bearer List Current Call)发生变化时,需要通知 GTBS Client 端,GTBS Client端收到后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u8 listLen;
    u8 currentListCall[STACK_AUDIO_CALL_MEMBERS_MAX_NUM*40];
} blc_gtbsc_listCurrentCallsEvt_t;

参数信息:

  • listLen: 列表总长度

  • currentListCall: 当前通话服务列表,格式为:

通话服务列表格式

对应的解析用结构体为:

typedef struct{
    u8 listItemLen;
    u8 callIndex;
    u8 state;
    u8 callFlags;
    u8 *pCallUri;
} blc_gtbsc_list_curr_call_t;

(6) AUDIO_EVT_GTBS_STATUS_FLAGS

当GTBS Server 端的状态标志(Status Flags)发生变化时,需要通知 GTBS Client 端,GTBS Client 端收到后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u16  statusFlags;
} blc_gtbsc_statusFlagsEvt_t;

参数信息:

  • statusFlags: 状态标志,位有效,具体含义为:

状态标志描述

(7) AUDIO_EVT_GTBS_INCOMING_CALL_TGT_URI

当GTBS Server端的来电目标服务商Incoming Call Target URI发生变化时,需要通知 GTBS Client 端,GTBS Client 端收到后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u8 uriLen;
    blc_tbs_incoming_call_target_bearer_uri_t uri;
} blc_gtbsc_incomingCallTgtUriEvt_t;

(8) AUDIO_EVT_GTBS_CALL_STATE

当GTBS Server端的通话状态(Call State)发生变化时,需要通知 GTBS Client 端,GTBS Client 端收到后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u8  stateLen;
    blc_gtbs_call_state_t state[STACK_AUDIO_CALL_MEMBERS_MAX_NUM];
} blc_gtbsc_listCallStateEvt_t;

通话状态的格式为:

typedef struct{
    u8 callIndex;
    u8 state;
    u8 callFlags;
} blc_gtbs_call_state_t;

(9) AUDIO_EVT_GTBS_TERM_REASON

当GTBS Server 端的通话结束原因(Terminate Reason)发生变化时,需要通知 GTBS Client 端,GTBS Client端收到后通过该事件上报给 APP 层。事件附带的参数信息结构体:

typedef struct{
    u8  callIndex;
    u8  termRsn;
} blc_gtbsc_termRsnEvt_t, blc_gtbss_terminationReasonNtf_t;

参数信息:

  • callIndex: Call Index;

  • termRsn: 通话断连原因,具体原因请参考枚举。

typedef enum
{
    GTBS_TERM_REASON_URI_ERROR                 = 0x00,
    GTBS_TERM_REASON_CALL_FAILED               = 0x01,
    GTBS_TERM_REASON_REMOTE_ENDED_CALL         = 0x02,
    GTBS_TERM_REASON_SERVER_ENDED_CALL         = 0x03,
    GTBS_TERM_REASON_LINE_BUSY                 = 0x04,
    GTBS_TERM_REASON_NETWORK_CONGESTION        = 0x05,
    GTBS_TERM_REASON_CLIENT_TERM_CALL          = 0x06,
    GTBS_TERM_REASON_NO_SERVICE                = 0x07,
    GTBS_TERM_REASON_NO_ANSWER                 = 0x08,
    GTBS_TERM_REASON_UNSPECIFIED               = 0x09,
}blc_gtbs_termReason_enum;

更多的操作参考SDK代码。

音量调整

音量的操作在LE Audio上有一个新名字,称为渲染和捕获控制,渲染和捕获控制主要包括 MICP 和 VCP 两个规范,在两个规范下又包括 MICS、VCS、VOCS、AICS 四个服务规范文档。

服务 描述
AICS 音频输入控制服务。定义了音频输入状态、输入类型、输入描述等相关的特性,可以设置输入的类型、状态、描述等。
VOCS 音量偏移控制服务。定义了与音量偏移、音频输出位置相关的特性。可以设置输出通道,输出描述等。
MICS 麦克风输入控制服务。定义了一个或多个麦克风的控制和状态,可以设置静音,取消静音等。
VCS 音量控制服务。定义了与音量调节、静音控制相关的特性。可以设置音量状态,音量控制点等。

耳机音量和音频输入服务的典型实现

上图显示了一副Server或带状助听器的典型实现,它具有三种可能的音频输入-蓝牙音频流、Telecoil 音频输入和麦克风输入。这些输入混合在一起,使用音频输入控制服务设置各自的增益,并有选择地静音和取消静音。结果流的增益由音量控制服务设置控制。如果流是立体声流,则一旦将其拆分为左右分量,音量偏移控制服务的单独实例就可以调整进入每个扬声器的增益。 (该图是硬件表示。实际上,VCS 和 VOCS 增益设置的总和应用于每个音频通道。)以不同的方式一起使用,它们模拟了平衡控制的效果。它们还可以单独用于调整每个扬声器中的相对声音级别,以适应左右耳不同程度的听力损失。

音量初始化

MICS初始化

注册客户端和服务端功能:

void blc_audio_registerMICSControlClient(const blc_micsc_regParam_t *param);
void blc_audio_registerMICSControlServer(const blc_micss_regParam_t *param);

Server parameter: 需要初始化的麦克风状态。空指针时,默认配置为defaultMicpParam定义的参数。麦克风状态的定义如下。

typedef enum{
MICS_MUTE_VALUE_NOT_MUTED = 0x00,
MICS_MUTE_VALUE_MUTED,
    MICS_MUTE_VALUE_DISABLED,
    MICS_MUTE_VALUE_RFU,
} blc_mics_mute_value_enum;

typedef struct{
    blc_mics_mute_value_enum mute;
} blc_micss_regParam_t;

const blc_micss_regParam_t defaultMicpParam =
{
    .mute = MICS_MUTE_VALUE_NOT_MUTED,
};

VCS/AICS/VOCS 初始化

SDK 设计时,AICS 可能出现被 MICS 和 VCS 同时包含的情况,AICS 的参数初始化在 VCS 初始化时完成。并且 SDK 默认的 AICS 不能只被 MICS 包含,而不被 VCS 包括,AICS 的控制只能全部通过 VCS 进行。

注册客户端和服务端功能:

void blc_audio_registerVCSControlClient(const blc_vcsc_regParam_t *param);
void blc_audio_registerVCSControlServer(const blc_vcss_regParam_t *param);

#define VCS_DEFAULT_STEP                1
#define VCS_DEFAULT_VOLUME          20

#define AICS_INPUT_DESC_1               "Telink AICS Input Description 1"
#define VOCS_OUTPUT_DESC_1              "Telink VOCS Output Description 1"

static const blc_aicss_regParam_t defaultAicsParam[APP_AUDIO_VCS_INCLUDE_AICS_INSTANCE_NUM] = {
#if APP_AUDIO_VCS_INCLUDE_AICS_INSTANCE_NUM > 0
    {
        .gainSetting = 1,
        .mute = AICS_MUTE_VALUE_MUTED,
        .gainMode = AICS_GAIN_MODE_VALUE_MANUAL,
        .units = 1,
        .minGain = -128,
        .maxGain = 127,
        .inputType = AICS_INPUT_TYPE_DIGITAL,
        .inputStatus = AICS_INPUT_STATUS_ACTIVE,
        .desc = AICS_INPUT_DESC_1,
    },
#endif
......
};

static const blc_vocss_regParam_t defaultVocsParam[APP_AUDIO_VCS_INCLUDE_VOCS_INSTANCE_NUM] = {
#if APP_AUDIO_VCS_INCLUDE_VOCS_INSTANCE_NUM > 0
    {
        .location = BLC_AUDIO_LOCATION_FLAG_FL,
        .volumeOffset = 2,
        .desc = VOCS_OUTPUT_DESC_1,
    },
#endif
......
};

const blc_vcss_regParam_t defaultVcpRendererParam = {
    .vcsParam = {
        .step = VCS_DEFAULT_STEP,
        .volume = VCS_DEFAULT_VOLUME,
        .mute = false,
    },
    .aicsParam = defaultAicsParam,
    .vocsParam = defaultVocsParam,
};

const blc_micss_regParam_t defaultMicpParam =
{
    .mute = MICS_MUTE_VALUE_NOT_MUTED,
};

下面是Server结构体参数以及说明,结构体主要包含了 VCS 参数,AICS 参数,VOCS 参数,下面依次介绍。

typedef struct{
blc_vcs_regParam_t vcsParam;
    /* Register parameters for Audio Input Control Services(AICS) */
    const blc_aicss_regParam_t* aicsParam;
    /* Register parameters for Volume Offset Control Services(VOCS) */
    const blc_vocss_regParam_t* vocsParam;
} blc_vcss_regParam_t;
VCS参数
typedef struct{
u8 step;        //Volume Setting Change Step;1-255
    /* Volume State */
    u8 volume;      //Volume Setting
    bool mute;      //mute
} blc_vcs_regParam_t;
  • step: 音量控制的步进。这是由于 VCS 规范中有音量加、音量减这类定性操作,Profile 在执行这类操作时,需要有一个步进来控制。
  • volume: 输出音量值。范围[0-255]。
  • mute: 输出静音。
AICS参数
typedef struct{
/* Audio Input State */
    s8 gainSetting;         // Gain_Setting range(-128 to 127)
    u8 mute;        // mute blc_aics_mute_value_enum
    u8 gainMode;    //blc_aics_gain_mode_value_enum
    /* Gain Setting Properties */
    u8 units;       //Gain Setting Units, 0.1dB
    s8 minGain;     //Gain Setting Minimum >= -128
    s8 maxGain;     //Gain Setting Maximum <= 127, -128<=minGain<maxGain<=127
    /* Audio Input Type */
    u8 inputType;   //blc_aics_audio_input_type_def_enum
    /* Audio Input Status */
    u8 inputStatus; //blc_aics_audio_input_status_enum
    /* Audio Input Description */
    char *desc;
} blc_aicss_regParam_t;
  • gainSetting: 输入增益。范围 [-128,127]。
  • mute: 输入静音。参考如下定义:
typedef enum{
    AICS_MUTE_VALUE_NOT_MUTED = 0x00,
    AICS_MUTE_VALUE_MUTED,
    AICS_MUTE_VALUE_DISABLED,
    AICS_MUTE_VALUE_RFU,
} blc_aics_mute_value_enum;
  • gainMode: 输入增益模式。参考如下定义:
typedef enum{
    AICS_GAIN_MODE_VALUE_MANUAL_ONLY    = 0x00,
    AICS_GAIN_MODE_VALUE_AUTOMATIC_ONLY,
    AICS_GAIN_MODE_VALUE_MANUAL,
    AICS_GAIN_MODE_VALUE_AUTOMATIC,
} blc_aics_gain_mode_value_enum;
  • units: 输入增益步进,单位 0.1dB。
  • minGain、maxGain: 输入增益最大最小值。
  • inputType: 输入类型。参考音频输入类型定义,SDK 中定义如下:
typedef enum{
    AICS_INPUT_TYPE_UNSPECIFIED = 0x00, //Unspecified Input
    AICS_INPUT_TYPE_BLUETOOTH,          //Bluetooth Audio Stream
    AICS_INPUT_TYPE_MICROPHONE,         //Microphone
    AICS_INPUT_TYPE_ANALOG,             //Analog Interface
    AICS_INPUT_TYPE_DIGITAL,            //Digital Interface
    AICS_INPUT_TYPE_RADIO,              //AM/FM/XM/etc
    AICS_INPUT_TYPE_STREAMING,          //Streaming Audio Source
    AICS_INPUT_TYPE_RFU,
} blc_aics_audio_input_type_def_enum;
  • inputStatus: 输入状态。参考如下定义:
typedef enum{
    AICS_INPUT_STATUS_INACTIVE = 0x00,
    AICS_INPUT_STATUS_ACTIVE,
    AICS_INPUT_STATUS_RFU,
} blc_aics_audio_input_status_enum;
VOCS参数
typedef struct{
    /* Volume Offset State */
    s16 volumeOffset;   //-255 to 255 MIN_VOLUME_OFFSET
    /* Audio Location */
    u32 location;   //BLC_AUDIO_CHANNEL_ALLOCATION_RFU
    /* Audio Output Description */
    char *desc;
} blc_vocss_regParam_t;
  • volumeOffset: 输出音量偏移值。范围是 [-255,255]。
  • location: 音频输出位置。参考音频位置定义。
  • desc: 输出描述,通常是 utf-8 字符串。

音量操作

VCS音量使用

Client端

获取Server端状态:

audio_error_enum blc_vcsc_getVolState(u16 connHandle, blc_vcs_volume_state_t* state);

设定不静音且减少相对音量:

ble_sts_t blc_vcsc_writeUnmuteOrRelativeVolDown(u16 connHandle);

设定不静音且增加相对音量:

ble_sts_t blc_vcsc_writeUnmuteOrRelativeVolUp(u16 connHandle);

设定绝对音量:

ble_sts_t blc_vcsc_writeSetAbsoluteVol(u16 connHandle, u8 volSetting);

设定不静音:

ble_sts_t blc_vcsc_writeUnmute(u16 connHandle);

设定静音:

ble_sts_t blc_vcsc_writeMute(u16 connHandle);
Server端

更新Server音量:

ble_sts_t blc_vcss_updateVolSetting(u16 connHandle, u8 volSetting);

更新Server静音状态:

ble_sts_t blc_vcss_updateMuteState(u16 connHandle, bool mute);

更新Server音量状态:

ble_sts_t blc_vcss_updateVolState(u16 connHandle, u8 volSetting, bool mute);

MICS麦克风输入控制

Client端

获取mute状态:

audio_error_enum blc_micsc_getMute(u16 connHandle, u8* mute);

写入mute状态

ble_sts_t blc_micsc_writeMute(u16 connHandle, blc_mics_mute_value_enum mute, prf_write_cb_t writeCb);

typedef enum{
    MICS_MUTE_VALUE_NOT_MUTED = 0x00,
    MICS_MUTE_VALUE_MUTED,
    MICS_MUTE_VALUE_DISABLED,
    MICS_MUTE_VALUE_RFU,
} blc_mics_mute_value_enum;
Server端

更新mute状态:

ble_sts_t blc_micss_updateMute(u16 connHandle, blc_mics_mute_value_enum mute);

Microphone控制服务的简单使用

AICS音频输入控制

一个设备可以有多个音频输入控制服务,主要用来实例化音频输入(如蓝牙音频流、麦克风等)的设置。多个音频输入可以成为音频混合功能的一部分。AICS 通常被 VCS 或者 MICS 给包含,被 MICS 包含的 AICS 的音频输入类型只能是麦克风。SDK 在 Server 部分做了一定的简化,认为 AICS 被 MICS 包含的同时,肯定被 VCS 包含,没有考虑只有麦克风输入无音频输出的设备。Client 端是完整的服务,AICS 能单独被 MICS 或 VCS 包含,也能同时被 MICS 和 VCS 包含。

音量控制服务

此场景类似于歌手佩戴的耳返,一方面可以听到乐队的背景音乐,这个背景音乐可调音量;一方面是自身的MIC,这个也可以调整音量。两者混合之后就是完整的耳返,可以随意调整听到的任何一个音频的输出幅度。但是这个技术需要音频混合,目前SDK不支持。

Client端

获取音频输入状态:

audio_error_enum blc_aiscc_getAudioInputState(u16 connHandle, blc_aics_client_t* aicsc, blc_aics_audio_input_state_t* inputState);

获取gain设置:

audio_error_enum blc_aiscc_getGainSetProperties(u16 connHandle, blc_aics_client_t* aicsc, blc_aics_gain_setting_properties_t* gainSetProp);

增益与描述

获取音频输入类型:

audio_error_enum blc_aiscc_getAudioInputType(u16 connHandle, blc_aics_client_t* aicsc, u8 type[1]);

typedef enum{
    AICS_INPUT_TYPE_UNSPECIFIED = 0x00, //Unspecified Input
    AICS_INPUT_TYPE_BLUETOOTH,          //Bluetooth Audio Stream
    AICS_INPUT_TYPE_MICROPHONE,         //Microphone
    AICS_INPUT_TYPE_ANALOG,             //Analog Interface
    AICS_INPUT_TYPE_DIGITAL,            //Digital Interface
    AICS_INPUT_TYPE_RADIO,              //AM/FM/XM/etc
    AICS_INPUT_TYPE_STREAMING,          //Streaming Audio Source
    AICS_INPUT_TYPE_RFU,
} blc_aics_audio_input_type_def_enum;

获取音频输入状况:

audio_error_enum blc_aiscc_getAudioInputStatus(u16 connHandle, blc_aics_client_t* aicsc, u8 status[1]);

typedef enum{
    AICS_INPUT_STATUS_INACTIVE = 0x00,
    AICS_INPUT_STATUS_ACTIVE,
    AICS_INPUT_STATUS_RFU,
} blc_aics_audio_input_status_enum;

获取音频输入描述:

audio_error_enum blc_aiscc_getAudioInputDescription(u16 connHandle, blc_aics_client_t* aicsc, u8* desc, u16* descLen);

MIC也是一样的流程,API请参考SDK。

写入音频输入控制端点:

ble_sts_t blc_aicsc_writeAudioInputControlPoint(u16 connHandle, blc_aics_client_t* aicsc, int opcode, s8 gainSetting, prf_write_cb_t writeCb);

写入设置的MIC gain大小:

ble_sts_t blc_aicsc_writeSetGainSettingByMicpIndex(u16 connHandle, int index, s8 gainSetting);

Gain Setting 在 Gain Setting Units 中公开当前的增益值,将值0写入 Mute 不会影响当前的 Gain Setting 值,因此通过向 Mute 写入1将其取消静音会将增益返回到 Gain Seting 中的先前值,如果Server处于自动增益模式,它将忽略写入 Gain Setting 字段的任何内容。

在更复杂的情况下,AICS 允许音频流自动控制其增益(传统上称为自动增益控制 AGC)或手动控制,这可以由音量控制Client或本地用户控制,并在音频输入状态特性中公开变更的值。当控制是自动时,Server不支持Client所做的任何更改,Server可以使用下图中显示的值,通过音频输入状态特性的 Gain Mode 字段公开是否允许Client将模式从自动更改为手动,反之亦然。

增益模式与描述

通过index设置MIC不静音:

ble_sts_t blc_aicsc_writeUnmuteByMicpIndex(u16 connHandle, int index);

通过index设置MIC静音:

ble_sts_t blc_aicsc_micpMute(u16 connHandle, int index);

设置MIC手动调整gain:

ble_sts_t blc_aicsc_micpSetManualGainMode(u16 connHandle, int index);

设置MIC自动调整gain:

ble_sts_t blc_aicsc_micpSetAutoGainMode(u16 connHandle, int index);

设置输入设备不需要回复的描述:

ble_sts_t blc_aiscc_writeInputDescWithoutRsp(u16 connHandle, blc_aics_client_t* aicsc, u8* desc, u16 descLen);

设置输入设备需要回复的描述:

ble_sts_t blc_aiscc_writeMicpInputDesc(u16 connHandle, int index, u8* desc, u16 descLen);
Server端

更新输入状态:

ble_sts_t blc_aicss_updateInputState(u16 connHandle, blc_aics_server_t *aicss, blc_aics_audio_input_state_t* inputState);

更新输入状况:

ble_sts_t blc_aicss_updateInputStatus(u16 connHandle, blc_aics_server_t *aicss, u8 status);

更新输入描述:

ble_sts_t blc_aicss_updateInputDesc(u16 connHandle, blc_aics_server_t *aicss, u8* desc, u16 descLen);

VOCS音量使用

audio_error_enum blc_vocsc_getVolOffsetState(u16 connHandle, blc_vocs_client_t* vocsc, blc_vocs_volume_offset_state_t* state);

audio_error_enum blc_vocsc_getAudioLoc(u16 connHandle, blc_vocs_client_t* vocsc, u32* location);

ble_sts_t blc_vocsc_writeAudioLoc(u16 connHandle, blc_vocs_client_t* vocsc, u32 location);

ble_sts_t blc_vocsc_writeVolOffsetCtrlPoint(u16 connHandle, blc_vocs_client_t* vocsc, blt_vocs_volume_offset_control_opcode_enum opcode, s16 volOffset, prf_write_cb_t writeCb);

音量事件上报

MICS事件

typedef enum{
    AUDIO_EVT_MICSC_START = AUDIO_EVT_TYPE_MICSC,
    AUDIO_EVT_MICSC_CHANGE_MUTE,    //refer to 'blc_micsc_muteChangeEvt_t'
} audio_micsc_evt_enum;

AUDIO_EVT_MICSC_CHANGE_MUTE:

MICS 客户端收到服务器上报麦克风静音状态后,上报的事件。

typedef enum{
    MICS_MUTE_VALUE_NOT_MUTED = 0x00,
    MICS_MUTE_VALUE_MUTED,
    MICS_MUTE_VALUE_DISABLED,
    MICS_MUTE_VALUE_RFU,
} blc_mics_mute_value_enum;

VCS事件

typedef enum{
    AUDIO_EVT_VCSC_START = AUDIO_EVT_TYPE_VCSC,
    AUDIO_EVT_VCSC_CHANGED_VOLUME_STATE,  //refer to 'blc_vcsc_volumeStateChangeEvt_t'
} audio_vcsc_evt_enum;

AUDIO_EVT_VCSC_CHANGED_VOLUME_STATE

typedef struct{ 
    u8   volumeSetting;
    bool mute;
} blc_vcsc_volumeStateChangeEvt_t;
  • volumeSetting:音量状态

    • 最大值 0xFF
    • 最小值 0x00
  • Mute: 静音状态

    • 输出静音 true
    • 输出取消静音 false

AICS事件

typedef enum{
    AUDIO_EVT_AICSC_START = AUDIO_EVT_TYPE_AICSC,
    AUDIO_EVT_AICSC_CHANGED_INPUT_STATE,    
} audio_aicsc_evt_enum;

AUDIO_EVT_AICSC_CHANGED_INPUT_STATE

typedef struct{
    u8 vcpInclIdx;
    u8 micpInclIdx;
    s8 gainSetting;
    u8 mute; //blc_aics_mute_value_enum
    u8 gainMode; //blc_aics_gain_mode_value_enum
} blc_aicsc_inStateChangeEvt_t;

VOCS事件

typedef enum{
    AUDIO_EVT_VOCSC_START = AUDIO_EVT_TYPE_VOCSC,
    AUDIO_EVT_VOCSC_CHANGED_VOLUME_OFFSET,      
    AUDIO_EVT_VOCSC_CHANGED_LOCATION,       
    AUDIO_EVT_VOCSC_CHANGED_OUTPUT_DESCRIPTION,
} audio_vocsc_evt_enum;

AUDIO_EVT_VOCSC_CHANGED_VOLUME_OFFSET

typedef struct{ 
    u8 vocsIndex;
    s16 volumeOffset;
} blc_vocsc_volumeOffsetStateChangeEvt_t;
  • vocsIndex: VOCS被VCP包含的索引号;
  • volumeOffset: 音量偏移状态值。范围是 [-255,255]。

AUDIO_EVT_VOCSC_CHANGED_LOCATION:

VOCS 客户端收到服务器上报音频位置后,上报的事件。

typedef struct{ 
    u8 vocsIndex;
    u32 location;
} blc_vocsc_locationChangeEvt_t;

AUDIO_EVT_VOCSC_CHANGED_OUTPUT_DESCRIPTION

typedef struct{
    u8 vocsIndex;
    u16 descLen;
    u8* desc;
} blc_vocsc_outputDescChangeEvt_t;
  • vocsIndex: VOCS 被 VCP 包含的索引号。
  • descLen: 音频输出描述长度。
  • desc: 音频输出描述。通常是 utf-8 格式。

USB

Telink USB 支持标准的 USB1.1 协议,传输速度支持低速及全速。采用外部供电,DM 与 DP 引脚与 GPIO 口复用 (DM-PA5,DP-PA6),如果用户不需要使用 USB 功能,也可以将 DM 与 DP 管脚配为普通的 GPIO 接口。支持的设备类包括 UDB、AUD、CDC、HID、USR。

Telink USB共有9个端点,端点0可同时用作输入和输出,其他端口只能用作单向输入或输出。端点0只能用作控制传输,包含建立阶段、数据阶段和状态阶段,用户可在接口描述符中使用bNumEndpoints端点来配置当前接口支持的端点数量。Telink USB使用8+256字节的USB专属内存来缓存各个端点的数据。端点0只支持8字节,其余端点共享256字节。

USB的数据发送和接收流程

数据发送

只有具备输⼊ (Device -> Host) 功能的端点才具备发送数据的功能。首先,用户发送数据时,应先通过reg_usb_ep_ctrl(i) 确定对应端口的繁忙状态。若此端口处于繁忙状态,则无法发送信息;若此端口处于空闲状态,则先 reset 此端口,随后通过 reg_usb_ep_dat(i) 往端口数据寄存器写入数据。数据长度由用户自己控制,但是注意不要超过在该端口的端口描述符中设置的数据最大长度。用户填充完数据后,将端口对应状态改为繁忙。随后,硬件会通过⼀个 IN 事务将数据传送给 Host,并清除对应端口的繁忙状态。

参考函数int usbaudio_hid_report(u8 report_id, u8 *data, int cnt)中的操作:

int usbaudio_hid_report(u8 report_id, u8 *data, int cnt){
    ...
    if(usbhw_is_ep_busy(USB_EDP_KEYBOARD_IN)){
    ...
    }
    reg_usb_ep_ptr(USB_EDP_KEYBOARD_IN) = 0;
    reg_usb_ep_dat(USB_EDP_KEYBOARD_IN) = report_id;
    foreach(i, cnt){
        reg_usb_ep_dat(USB_EDP_KEYBOARD_IN) = data[i];
    }
    reg_usb_ep_ctrl(USB_EDP_KEYBOARD_IN)=FLD_EP_DAT_ACK| (edp_toggle[USB_EDP_KEYBOARD_IN] ? FLD_USB_EP_DAT1 : FLD_USB_EP_DAT0);  // ACK
    edp_toggle[USB_EDP_KEYBOARD_IN] ^= 1;
    return 1;
}

数据接收

只有具备输出(Host -> Device) 功能的端口才具备接收数据的功能。设备接收到数据时,首先需要通过读取reg_usb_ep_ptr(i) 对于寄存器的值获取到设备接收到数据的长度,随后将该寄存器置 0。然后循环通过读取reg_usb_ep_dat(i) 获取到接收数据,并将数据存入接收数据缓存区。最后,通过操作 reg_usb_ep_ctrl(i) 将对应端点的ACK状态置 1。

参考函数void app_usb_irq_proc (void)中的操作:

if (usbhw_get_eps_irq()&FLD_USB_EDP6_IRQ)
{
    u8 usbData[256];
    unsigned char len = reg_usb_ep6_ptr;
    usbhw_reset_ep_ptr(USB_EDP_SPEAKER);
    for (unsigned int i=0; i<len; i++)  
    {
        usbData[i] = reg_usb_ep6_dat;
    }
    tlk_buffer_write(usbData,len,TLK_BUFFER_1);
    usbhw_data_ep_ack(USB_EDP_SPEAKER);
    usbhw_clr_eps_irq(FLD_USB_EDP6_IRQ);
}

Client USB

LE Audio CIS Demo Client 的USB主要用来作为音频ISO的接口,Speak和MIC的数据192+32已经用到224字节,剩余的可用做HID。HID在SDK里已经默认开启,用户直接使用即可。

USB的初始化

void blc_audio_usb_init(void) 

函数定义了USB的初始化,设置了端点的buffer大小,管脚的初始化,以及HID的回调。

USB命令的处理

void usb_handle_irq(void)

函数描述了对USB命令的处理,包括端点的setup、data、state、intf。USB标准命令主要在此处处理,用户可根据需要来处理所需数据。SDK集成了一个处理命令的回调函数:int my_hid_report_cb(u8 *ps, int len)。

app_usb_handle_set_intf

函数用来处理主机端下发的Speak和MIC的开关命令,通过这个命令,即可知道主机端有没有对Speak和MIC进行操作。

void my_app_cmd_rcvd(u8 cmd, u8 data)

对主机端主动上报一些定义的HID操作,设备端在初始化的时候就上报了主机应该以什么样的时间来轮询设备的HID信息,这个事件在描述符里面设置。

USB ISO的处理

void  app_usb_irq_proc (void) 

函数对ISO的数据进行处理,包括Speak的输出和MIC的输入。

Client Boot USB

USB初始化

void dfu_usb_init(u16 id)

函数定义了USB的初始化。

USB数据处理

void dfu_usb_handle_irq(void)

函数对USB命令,和数据的处理。

int hid_ouput_handle(void) 

函数中的callback有flash状态转变,主要是用USB来做数据的搬运,将固件更新到芯片的flash。

参考固件升级章节中的Client USB 升级

Server USB

Server USB 主要用来debug,也可以用作数据的搬运。Server枚举了一个UDB设备,实现了两个接口,分别是UBG和VCD,UBG主要用来打印log信息,也可以用来接收命令调试函数等,VCD配合上位机工具使用,主要用于log的图形化显示。

USB初始化

int tlkapi_debug_init(void)

函数定义了USB的初始化。

USB数据处理

void udb_usb_handle_irq(void) 

函数对命令和数据的处理,包括端点的setup、data、state、intf,USB标准命令的所有处理几乎都在这里,用户可根据需要来处理自己想要的数据。

int myudb_hci_cmd_from_usb (void) 

函数解析接收到的USB数据。

UDB 打印功能的实现

SDK目前封装了三个打印的接口,可用于不同环境下的打印情况。

#define  my_dump_str_data(en,s,p,n)            if(en){usb_send_str_data(s,(u8*)(p),n);}
#define  my_dump_str_u32s(en,s,d0,d1,d2,d3)    if(en){usb_send_str_u32s(s,(u32)(d0),(u32)(d1),(u32)(d2),(u32)(d3));}
#define  my_dump_str_u8s(en,s,d0,d1,d2,d3)     if(en){usb_send_str_u8s(s,(u8)(d0),(u8)(d1),(u8)(d2),(u8)(d3));}

示例接口如下:

(1) my_dump_str_data(APP_DUMP_EN, "free mem fail", &index, 1); (2) my_dump_str_u32s(APP_DUMP_EN,"usbaud_handle_set_feature_mute",id,mute, usbaud_mute, b);

VCD调试

(1) vcd 初始化

TBD,会在后续版本更新

(2) vcd 使能

TBD,会在后续版本更新

(3) vcd设置

TBD,会在后续版本更新

(4) vcd调试

TBD,会在后续版本更新

UART

Telink UART 一共有两个,分别为UART0和UART1。为了方便用户进行调试,SDK 目前集成了UART 打印,在Client中,log 的输出必须依靠UART ,因为USB 已经被用来做USB ISO。SDK 提供了三种调试手段,第一种是传统的UART 调试,采用硬件UART 可以在中断使用,使用DMA的机制,波特率常用1M。第二种是模拟的UART调试,不建议在中断使用。第三种是shell 组件UART,常用来调试函数。

目前默认的,Client 中UART0 用来打印系统log,UART1 用来收发UART 常规数据,采用shell 组件形式。Server 的UART目前没有使用,后续可能会用来作为有线升级的接口。UART具体数据的发送和接收范例请参考Telink 驱动文档。

UART通信原理

Telink UART 中,需要被发送的数据首先被芯片的 MCU 或者 DMA 写入TX缓冲区,然后UART模块通过 TX 引脚将 TX 缓冲区中的数据发送到其他设备。在接收数据的设备中,数据首先通过 UART 模块的RX 引脚被写入 RX 缓冲区,然后数据被接收设备的 MCU 或者 DMA 读取。如果芯片的 RX 缓冲区接近溢出状态,芯片会通过其 RTS 引脚发出一个信号(可以配置为高电平或者是低电平)给与其连接设备,即表明该设备应该停止继续发送数据。与其类似,如果芯片通过其 CTS 引脚接收到了一个信号,即表明其他设备的 RX 缓冲区接近溢出,芯片应该停止继续发送数据。

硬件UART

在驱动手册上,硬件UART使用DMA 模式收发波特率可以达到2M,但是正常情况下,为了稳定起见,我们通常使用1M的波特率,硬件UART DMA模式可以在中断中使用。

//Define UART TX pin : UART0(PA3 PB2 PD2), UART1(PC6 PD6 PE0)
typedef enum{
    UART0_TX_PA3 = GPIO_PA3,
    UART0_TX_PB2 = GPIO_PB2,
    UART0_TX_PD2 = GPIO_PD2,
    UART1_TX_PC6 = GPIO_PC6,
    UART1_TX_PD6 = GPIO_PD6,
    UART1_TX_PE0 = GPIO_PE0,
    UART_TX_NONE_PIN =0xfff,
}uart_tx_pin_e;

//Define UART RX pin : UART0(PA4 PB3 PD3), UART1(PC7 PD7 PE2)
typedef enum{
    UART0_RX_PA4 = GPIO_PA4,
    UART0_RX_PB3 = GPIO_PB3,
    UART0_RX_PD3 = GPIO_PD3,
    UART1_RX_PC7 = GPIO_PC7,
    UART1_RX_PD7 = GPIO_PD7,
    UART1_RX_PE2 = GPIO_PE2,
    UART_RX_NONE_PIN =0xfff,
}uart_rx_pin_e;

UART使能配置(DMA TRX)

uart_reset(UART1);
uart_set_pin(UART1_TX_PC6,UART1_RX_PC7);
uart_cal_div_and_bwpc(baudrate, sys_clk.pclk*1000*1000, &div, &bwpc);
uart_set_rx_timeout(UART1, bwpc, 12, UART_BW_MUL2);
uart_init(UART1, div, bwpc, UART_PARITY_NONE, UART_STOP_BIT_ONE);
uart_set_tx_dma_config(UART1, SHELL_UART_DMA_TX);
uart_set_rx_dma_config(UART1, SHELL_UART_DMA_RX);
uart_clr_tx_done(UART1);
uart_set_irq_mask(UART1, UART_RXDONE_MASK);
uart_set_irq_mask(UART1, UART_TXDONE_MASK);
plic_interrupt_enable(IRQ18_UART1);
core_interrupt_enable();
uart_receive_dma(UART1, shell_buff.buffer, SHELL_BUFF_SIZE);

模拟UART

模拟UART跟硬件UART在使用上并没有显著差异,唯一的区别是不建议在中断中使用,因为模拟UART的打印会占用CPU的资源,且会影响时序。

UART打印

打印log实现可以参考 vendor/common/tlkapi_debug.c 和tlkapi_debug.h。

#define TLKAPI_DEBUG_CHANNEL_UDB 1 //USB Dump
#define TLKAPI_DEBUG_CHANNEL_GSUART 2 //GPIO simulate UART
#define TLKAPI_DEBUG_CHANNEL_UART 3 //hardware UART

#ifndef TLKAPI_DEBUG_ENABLE
#define TLKAPI_DEBUG_ENABLE 0
#endif

#ifndef TLKAPI_DEBUG_CHANNEL
#define TLKAPI_DEBUG_CHANNEL TLKAPI_DEBUG_CHANNEL_UDB
#endif

void tlkapi_send_str_data (char *str, u8 *ph, u32 n);
void tlkapi_send_str_u8s (char *str, u8 d0, u8 d1, u8 d2, u8 d3);
void tlkapi_send_str_u32s (char *str, u32 d0, u32 d1, u32 d2, u32 d3);
int my_printf(const char *fmt, ...);
#define tlkapi_send_string_data(en,s,p,n) if(en){tlkapi_send_str_data(s,(u8*)
(p),n);} 
#define tlkapi_send_string_u8s(en,s,d0,d1,d2,d3) if(en){tlkapi_send_str_u8s(s,(u8)(d0),(u8)(d1),(u8)(d2),(u8)(d3));} 
#define tlkapi_send_string_u32s(en,s,d0,d1,d2,d3) if(en){tlkapi_send_str_u32s(s,(u32)(d0),(u32)(d1),(u32)(d2),(u32)(d3));} 
#define tlkapi_printf(en,fmt, ...) if(en){my_printf(fmt, ##__VA_ARGS__);}

示例:

tlkapi_printf (1, "PC POWER ON\n", &last_status, 1);
BLT_APP_STR_LOG("[APP]adv mac", pExtAdvInfo->address, 6);

在stack/ble/profile/audio/common/audio_cfg.h中,有各层log 的开关,用户可根据需要打开相应log,以下是部分范例:

///////////////// Audio profile debug log //////////////
#ifndef DBG_PRF_AUD_LOG
#define DBG_PRF_AUD_LOG                 1
#endif
#ifndef PRF_DBG_AUDIO_EN
#define PRF_DBG_AUDIO_EN                    1
#endif
#ifndef PRF_DBG_ASCS_EN
#define PRF_DBG_ASCS_EN                 1
#endif

#ifndef PRF_DBG_BASS_EN
#define PRF_DBG_BASS_EN                     1
#endif

Shell组件UART

Shell 组件其实主要是对UART发送接收的内容进行处理。这个组件很好的解决UART调试困难的问题,使用常见的UART上位机就能对函数进行调试。此外,这个组件也是可以打印log的。

Shell初始化

uart_init_log(1000000);
shell_init();
shellTask(&shell);

在正常的UART初始化后加上shell的初始化,且在main_loop 加上task。目前SDK已经把shell 集成,用户只需要使能宏定义就可以使用shell。

#define UART_SELECT_SHELL_ENABLE   1

Shell的使用

(1) 打印log

#define PRINTK(...)      trace(__VA_ARGS__)
#define LOG(...)        do{PRINT("[INF]: "__VA_ARGS__);}while(0)
#define LOG_ERR(...)    do{PRINT("[ERR]: "__VA_ARGS__);}while(0)
#define LOG_WAR(...)    do{PRINT("[WAR]: "__VA_ARGS__);}while(0)

示例:

PRINTK("ptcmd cmd error  \r\n ");
PRINTK(" %x ", buff[i]);

(2) 调试函数 在正常功能下,shell 可以用来调试函数,如下示例:

func(int i, char ch, char *str)
{
    printf("input int: %d, char: %c, string: %s\r\n", i, ch, str);
}

定义:SHELL_EXPORT_CMD(func, func, test)

输入:letter>>func 666 'A' "hello world"

输出:input int: 666, char: A, string: hello world

这个组件目前适配了xcom、putty等,其他的上位机软件用户可以自行测试。

固件升级

Client/Server EVB 升级

(1) 打开 BDT 工具,点击 Device 选项选择对应的 EVB 黑盒子,接着选择下方的芯片为B91(当前版本),再打开File选项选择对应的固件,即可直接下载。

(2) 如果第一步下载失败,用户可在完成Activate操作后,再次尝试下载。

(3) 如果第二步失败,则需要使用杜邦线将3.3、SWS、GND三个线接到芯片对应的管脚上,完成连接后再次执行第二步操作(注意:此时需要断开黑盒子的USB接口)。

(4) 如果上述步骤都失败了,请联系Telink FAE请求技术支持。

显示界面

Client USB 升级

DFU使用

Client 调用CMD_DFU操作进入bootloader后,用户只需将固件拖放到main.exe 应用程序中,即可自动完成固件的升级,升级完成后,该程序自动复位。

analog_write(0x3c, 0x4b);
cpu_sleep_wakeup_32k_rc(DEEPSLEEP_MODE, PM_WAKEUP_TIMER, clock_time() + SYSTEM_TIMER_TICK_1MS * 1000);

(1) USB DFU

通过 analog_read,当返回值为 0x4d 时(在应用代码里通过 analog_write(0x3c, 0x4b)设置),进入USB DFU状态,USB host端可发现名为“USB DFU”的USB设备。如果30秒内未收到DFU相关指令,则设备将重新启动。

(2) 启动应用代码

当返回值非 0x4d 时,bootloader将正常启动应用代码:首先对应用代码进行完整性检查,若检查通过,则跳转到应用代码的start up部分。

若检查完整性失败,则进入USB DFU状态。

日志如下:

Telink(C)2022 USB iap tool
Powered by HIDAPI library
Usage: Drag DFU image file to this execute file !
OTA file size is 235124
Manufacturer String: Telink
Product String : USB DFU
[IAP]: flash unlock
[IAP]: Erase @ 0x00008000
[IAP]: Erase @ 0x00009000
[IAP]: Erase @ 0x0000a000
[IAP]: Erase @ 0x0000b000
......
[IAP]: Erase @ 0x00037000
[IAP]: Erase @ 0x00038000
[IAP]: Erase @ 0x00039000
[IAP]: flash check from 00008000, size 202356
[IAP]: Response CRC:41429222 local_crc:41429222
[IAP]: flash reboot
Firmware updated successfully !

升级成功后,上位机将打印Firmware updated successfully。100 ms后,Client将应用的新固件重新启动。

DFU修改

修改USB描述符

在usb_dbg/dfu_usbdesc.c 中,可以修改USB常用的 device/config 描述符。

其他常用的字符串,可以通过如下宏进行修改,如下宏位于 dfu_usbdesc.h 文件中:

#define MYDFU_STRING_VENDOR L"Telink"
#define MYDFU_STRING_PRODUCT L"USB DFU"
#define MYDFU_STRING_SERIAL L"TLSR9218"

HID 描述符可以通过“hid_report_descriptor1”进行修改。

USB数据接收

USB协议栈,通过 dfu_usb_handle_irq 实现。当检测到output数据时,会触发对 usb_hid_output_req 函数的回调,该回调事件实际是由 hid_output_handle 函数触发。

_attribute_ram_code_ int hid_output_handle(void)
{
    uint8_t data[64];
    if (reg_usb_ep_irq_status & EP_IRQ_HID_OUTPUT)
    {
        reg_usb_ep_irq_status = EP_IRQ_HID_OUTPUT;
        reg_usb_ep_ptr(EP_HID_OUTPUT) = 0;
        for (int i=0; i <  64; i++)
        {
            data[i] = reg_usb_ep_dat(EP_HID_OUTPUT);
        }
        reg_usb_ep_ctrl(EP_HID_OUTPUT) = 1;     //ACK
        extern void usb_hid_output_req(uint8_t *p_dat, int len);
        usb_hid_output_req(data, 64);
    }
    return 0;
}

Server夹具固件升级

TBD,会在后续版本更新

Server无线APP升级

TBD,会在后续版本更新

FLASH地址分配

目前Server与Client在SDK中使用的是1M/2M的flash,地址的分配和常用用户保存如下。如用户需使用更大的flash,请联系Telink 技术支持。

Server 1M 地址分配

TBD,会在后续版本更新

Server 2M 地址分配

TBD,会在后续版本更新

Client 1M 地址分配

地址 大小 功能
0x00000000~0x00007FFF 32KB Bootloader
0x00008000~0x00008FFF 4KB 固件信息(CRC32+size+version)
0x00009000~0x000E0FFF 830KB 固件
0x000E1000~0x000E7FFF 28KB 保留位
0x000EB000~0x000EBFFF 4KB CALIB_OFFSET_CAP_ADDR
0x000EC000~0x000ECFFF 4KB FLASH_DONGLE_NAME
0x000ED000~0x000EDFFF 4KB USER_CONST_BASE
0x000EE000~0x000EEFFF 4KB SW version
0x000EF000~0x000EFFFF 4KB HW version
0x000F0000~0x000F3FFF 16KB Audio ATT SDP info
0x000F4000~0x000F7FFF 16KB SMP pairing and key information area
0x000F8000~0x000FBFFF 16KB Secure Boot descriptor
0x000FC000~0x000FDFFF 8KB 保留位
0x000FE000~0x000FDFFF 4KB 内置电容校准值
0x000FF000~0x000FFFFF 4KB 蓝牙地址

Client 2M 地址分配

地址 大小 功能
0x00000000~0x00007FFF 32KB Bootloader
0x00008000~0x00008FFF 4KB 固件信息(CRC32+size+version)
0x00009000~0x001DFFFF 1880KB 固件
0x001E1000~0x001E4FFF 16KB 保留位
0x001E5000~0x001E5FFF 4KB CALIB_OFFSET_CAP_ADDR
0x001E6000~0x001E6FFF 4KB FLASH_DONGLE_NAME
0x001E7000~0x001E7FFF 4KB SW version
0x001E8000~0x001E8FFF 4KB HW version
0x001E9000~0x001E9FFF 4KB USER_CONST_BASE
0x001EA000~0x001EBFFF 8KB Audio ATT SDP info
0x001EC000~0x001EFFFF 16KB SMP pairing and key information area
0x001F0000~0x001F7FFF 32KB SW align area
0x001F8000~0x001FBFFF 16KB Secure Boot descriptor
0x001FC000~0x001FDFFF 8KB 保留位
0x001FE000~0x001FEFFF 4KB 内置电容校准值
0x001FF000~0x001FFFFF 4KB 蓝牙地址

Flash使用

Flash的Page读/写、Sector 4K的擦除都是在关闭中断条件下进行操作,而其他函数未关闭中断。进行flash操作时,需要关闭中断进行。

  • 读取数据:
void flash_dread(unsigned long addr, unsigned long len, unsigned char *buf)
  • 写入数据:
void flash_page_program(unsigned long addr, unsigned long len, unsigned char *buf)
  • Flash 擦除:
void flash_erase_sector(unsigned long addr)

所有的定义都存储在SDK的drivers/B91/flash.c文件中。

电压检测

Server端在作为产品时,通常采用电池作为供电源,此时需要知道电池的状态,需要使用ADC采集电池的电压。在TLSR951x系列中,支持采样的GPIO为PB0-7/PD0-1共10个IO口。在SDK中,Server使用PB3作为采样口。

电压检测初始化

(1)使用接口初始化:

void check_bat_init(void)

(2)采样口初始化:

void adc_gpio_sample_init(adc_input_pin_def_e pin,adc_ref_vol_e v_ref,adc_pre_scale_e pre_scale,adc_sample_freq_e sample_freq)

参数:

  • Pin: 管脚选择
  • v_ref:参考电压选择,建议只选择ADC_VREF_1P2V
  • pre_scale:预分压系数,建议只选择ADC_PRESCALE_1F4
  • sample_freq:采样率

(3)设置采样数和去掉无用数据数值:

#define ADC_BUF_SIZE    32
#define ADC_BUF_DELETE  8

(4)如果使用DMA传输:

  • 需在初始化时设置DMA:
#define  ADC_DMA_CHN    DMA7
  • 在adc_gpio_sample_init初始化后配置DMA:
adc_set_dma_config(ADC_DMA_CHN);
  • 此时在获取ADC数值时也需换成带DMA后缀的API:adc_get_voltage_dma()。SAR_ADC只能使用udio的FIFO1(而不是FIFO0),并且FIFO1不能同时用于SAR_ADC和audio,FIFO0只能用于音频。
audio_data_fifo1_path_sel(SAR_ADC_DATA_IN_FIFO,OUT_NO_USE);

电压检测使用

使用框图如下图所示:

采样流程图

手动 (ADC_NDMA_MODE) 采样时,一次只能获取一个 adc_code,在此期间会关闭ADC 采样功能,且在使用时一定要保证连续获取 adc_code 的时间间隔大于 2 个采样周期(如果是96 kHz,那么时间是为10.4 \(\mu\)s)。

  • 获取采样值且对采样值处理:
u16 adc_get_voltage(void)

函数每15 ms采样一次(这个数据可以变化)。用户打开开关立刻采样随后关闭开关。当采完32个数据后进行排序,将前8个和后8个数据去掉后取平均值(采样多少个数据和去掉多少个用户可以自己决定),然后将这个平均值和中位数比较(不一定是中位数,位于中间即可),如果两者差异不大,则说明这次采样是准确的,如果差异过大,那么代表这个采样是不准确的,会采用上一次的取值作为这次的采样值。

  • 获取对应的电池电压值:
void check_bat_state(void)

函数获得对应的电池电压值,这个电压值是作为一个表格的成员存在,从0到100一共101个单位。每过一秒钟获取一次比较之后的数据,并且连续采集5次数据再平均,然后再比较上一次的值,如果有变化,则会更新状态,并且通知Client 或者手机。同时,检测获得的数据是不是在警戒线上,如果连续在警戒线上,则执行相应的操作,比如低电提示、超低电关机等。

  • 充电状态:
void system_enter_charging(void)

函数进入充电状态。一旦进入充电状态,那么会断开ACL的连接,且进入休眠模式。存在一个IO口作为唤醒口,通过这个口唤醒Server。

电压检测修改

目前SDK支持一个IO采样,如果用户需要多通道采样,需在使用u16 adc_get_voltage(void)之前先使用下面代码切换pin。

adc_pin_config(ADC_GPIO_MODE, pin);
adc_set_diff_input(pin >> 12, GND);

按键检测

SDK中主要有两套按键检测模板,均位于Server端,Client端的按键检测此处不作介绍,仅作为实验使用。第一套为单个按键实现,用于TLSR9517音频开发板的按键实现,相关代码存放在app_key.c和app_key.h;第二套为矩阵键盘实现,用于TLSR9518开发板上四个按键的操作,相关代码存放在app_ui.c和app_ui.h。这里只对第一套按键做介绍,第二套请用户参考SDK。

按键检测目前在SDK中实现了单击、双击、三击、长按x秒的功能,能够满足用户各种使用场景。且按键功能集成度高,用户只需要稍微改动便能轻松设置。

按键检测初始化

初始化按键:

void key_init(void)

初始化按键并设置低功耗PM触发管脚:

  • 设置最大按键数量:
#define KEY_NUM     3
  • 设置按键扫描间隔和按压间隔:
#define KEY_SCAN_INTERVAL     10000
#define KEY_CLICK_INTERVAL    500000
  • 设置按键的管脚:
#define WAKEUP_PAD_ONKEY            GPIO_PB0
#define VOL_UP_KEY_PIN              GPIO_PB2
#define VOL_DOWN_KEY_PIN            GPIO_PB1
  • 设置长按时间:
#define KEY_LONG_PRESS_2S     4
#define KEY_LONG_PRESS_5S     10
  • 配置按键信息且设置触发电平:
key_ctl_t key_cfg[KEY_NUM] =
{
    {
        .pin = WAKEUP_PAD_ONKEY,
        .key_down_level = 0,    
    },
......
};

按键检测使用

  • 按键扫描和触发:
void key_task(void)
  • 单击、双击、三击和长按2s:
void power_key_sclick_func(void)
void power_key_dclick_func(void)
void power_key_tclick_func(void)
void power_key_long_func(void)
void vol_up_key_sclick_func(void)
void vol_up_key_dclick_func(void)
void vol_up_key_tclick_func(void)
void vol_up_key_long_func(void)
void vol_down_key_sclick_func(void)
void vol_down_key_dclick_func(void)
void vol_down_key_tclick_func(void)
void vol_down_key_long_func(void)

按键检测修改

目前SDK中使用了3个按键,若需减少按键数量:

第一步:修改KEY_NUM 数量;

第二步:删除相应的管脚宏定义;

第三步:删除key_cfg 里相应的配置;

第四步:删除对应函数及app_key_event_handle 中的配置。

如果需要增加按键,则参照上述步骤逆向操作即可。

如果要更改长按时间,需要在key_task里面修改,或者在头文件宏定义中修改时间,然后对应更新task。

#define KEY_LONG_PRESS_2S     4
#define KEY_LONG_PRESS_5S     10

按键的其他定义,请用户自行查阅代码:app_key.c 和 app_key.h。

LED管理

为了统一SDK的灯效管理,Server端集成了一套LED管理程序,该程序集成度高,不需要太多修改便能实现所需的功能。在Client端未执行类似的操作,仅简单拉高拉低电平,如需要,请参考Server端实现。

LED初始化

设定LED开启的电平:

#define LED_ON_LEVEL    1

设定LED的数量:

#define LED_NUM     3

设定LED的管脚:

#define GPIO_LED_BLUE           GPIO_PD3
#define GPIO_LED_GREEN          GPIO_PD2
#define GPIO_LED_WHITE          GPIO_PD1
#define GPIO_LED_RED            GPIO_PD0

设定LED的状态:

const led_flash_t led_flash_cfg[] =
{
    {STATE_OFF,             0,      0,          100,            0},
    {STATE_ON,              0,      5000000,    0,              0},
    {STATE_ON,              0,      500000,     100,            0},
    {STATE_BLINK,           5,      200000,     200000,             0},
    {STATE_BLINK,           2,      200000,     200000,         5000000},
    {STATE_BLINK,           1,      200000,     100000,         100000},
};
typedef struct {        
    led_state state;    //led state
    unsigned int blink_cnt;   //blink count
    unsigned int on_time;       //led on times
    unsigned int off_time;      //led off times
    unsigned int interval_time; //led blink interval times
}led_flash_t;
typedef enum
 {   
    STATE_OFF,
    STATE_ON,
    STATE_BLINK,
    STATE_BLINK_REPEAT
}led_state;

参数:

  • State:打开、关闭、闪烁x次、重复闪烁
  • blink_cnt:闪烁次数
  • on_time:打开时间
  • off_time:关闭时间
  • interval_time:闪烁间隔

LED的初始化:

void leds_init(void)

设定上电默认LED状态:

const led_config_t led_default_config[LED_NUM] = {
    { 
        .led_pin = GPIO_LED_RED,
        .pattern.state = STATE_ON,
        .pattern.blink_cnt = 0,
        .pattern.on_time = 1000000,
        .pattern.off_time = 5000,
    },
......
};

LED使用

LED的运行:

void leds_task(void)

单个LED的管理:

void set_led_state(led_index_t index, led_pattern_index_t led_pattern) 

参数:

  • Index:对应的LED索引
  • led_pattern:LED模式
typedef enum
{
    LED_INDEX_RED = 0,
    LED_INDEX_GREEN,
    LED_INDEX_BLUE,
    LED_INDEX_MAX
}led_index_t;
typedef enum
{
    LED_PATTERN_LED_OFF = 0,
    LED_PATTERN_LED_ON,
    LED_PATTERN_LED_ON_500MS,
    LED_PATTERN_LED_FLASH_5_TIMES,
    LED_PATTERN_LED_FLASH_2_TIMES_IN_5S,
    LED_PATTERN_LED_FLASH_200MS,
    LED_PATTERN_LED_MAX_STATE,
}led_pattern_index_t;

示例:

set_led_state(LED_INDEX_RED, LED_PATTERN_LED_ON_500MS);

函数表示红灯点亮500ms,500ms后关闭。

set_led_state(LED_INDEX_BLUE, LED_PATTERN_LED_FLASH_2_TIMES_IN_5S);

蓝灯每间隔5s会连续闪烁两次,每次的开关时间为200ms。

LED修改

如果用户需要修改LED的变化形式,需要同时修改LED模式和LED状态设定,然后才可以直接用set_led_state配置所需要的场景。

目前SDK支持三个LED管理,如果用户需减少 LED 数量:

(1)需要修改宏配置数量:

#define LED_NUM     3

(2)然后删除函数led_default_config对应的LED预配置。

如果用户需要使用更多的LED,那么参照删除步骤逆向操作即可增加LED数量。

产品测试

目前SDK对Client做了产测的命令,Server端暂未实现。

USB UDB命令

USB UDB命令是Server使用的产测方式,Client无法使用。Client的工具为USB HID和UART。

  • 注册命令回调:
myudb_register_hci_debug_cb(my_debug_server);

注册完成后就可以使用UDB命令服务,具体的实现参考USB章节。

  • 处理函数:
int my_debug_server(unsigned char *p, int len)
{
    if (p[0] != 0x11)
    {
        return 1;
    }
    switch (p[1])
    {
        case 0:
        break;
        case 1:
        break;
......
        default:
        break;
    }
    return 0;
}

目前Server产测尚未添加具体命令。待添加。

  • 上位机工具:

操作示意图

USB HID 命令

  • 注册回调:
register_usb_debug_report_cb (my_hid_report_cb);
  • 处理函数:
int  my_hid_report_cb(u8 *ps, int len)
{
......
    switch (cmd)
    {
        case (DFUCMD_REM | DFUCMD_REM_DFU):
        break;
......
        default:
        break;
    }
......
    usb_custom_hid_report_set (rsp, 31);
    return 0;
}

处理函数目前集成了Client产测所需要的命令,具体的命令请参考Client产测命令xls表格。

  • 上位机工具:

操作说明

UART命令

UART 产测命令使用了shell组件,所以需要在shell/port.h中打开shell enable,然后选择相应的UART。

#define UART_SELECT UART1_SHELL 
#define UART_SELECT_SHELL_ENABLE   1
  • 处理函数:
void production_test_cmd(int cmd, int data)
void production_test_enter_exit(int cmd)
void production_test_set_mac(int cmd, int buf1, int buf2, int buf3, int buf4, int buf5, int buf6)
void production_test_set_emi(int cmd, int buf1, int buf2, int buf3, int buf4, int buf5)

UART 产测目前集成了四个函数,这四个函数能够实现 Client 产测所需要的所有功能,具体的命令请用户参考Client产测命令 xls 表格。

  • 上位机工具:

目前 UART 上位机开发时只对 xcom 和 putty 进行了测试,其他的软件请用户自行测试。

测试详情

其他命令

其他的测试工具和命令SDK尚未集成,用户可自行实现。

附录

连接失败类型及定位解决办法

TBD,会在后续版本更新

基于LE Audio CIS带宽评估方法

目前SDK使用LC3编解码协议,具体的数据量如下图:

数据量表格

带宽计算方法:

以CIS interval 10ms为例计算带宽,使用BLE 2M。

(1) 一个标准的BLE数据包,除了数据之外,还需要有preamble.access code、header、MICCRC共120bits,使用2M PHY耗时60\(\mu\)s,该时间是固定的。

(2) 帧间间隔固定150 \(\mu\)s,两个subevent之间的间隙至少为150\(\mu\)s。

(3) 每个包的具体长度,取决于包所携带的数据长度。

(4) 为了保证传输质量,根据经验,每个包的重传次数(subevent的个数)建议至少为3个。

(5) 无论是几个ACL,留给ACL的带宽至少为1.875ms。

示例:

上下行都以16kHz、10ms、16bits为例,经过LC3压缩,数据长度为40 Bytes,则一个subevent的时间为 T=60 \(\mu\)s+160 \(\mu\)s+60 \(\mu\)s+160 \(\mu\)s+150 \(\mu\)s+150 \(\mu\)s=740 \(\mu\)s,160 \(\mu\)s的来源为: 40 Bytes数据共320bits,2M PHY下时长160 \(\mu\)s。

示意图

LE Audio CIS Demo SDK 资源消耗

TBD,会在后续版本更新