跳转至

tl_bluetooth_audio_sdk 开发手册


概述

随着音频市场的发展,Telink 音频产品线已覆盖 BT/BLE 双模、BT/TPSLL(Telink Proprietary Synchronous Link Layer)低延时双模混音(头戴/TWS)及 LE Audio 等方向,并已在众多客户项目中实现量产落地。为满足不断增长的市场需求并适配更多 Audio 芯片平台,Telink 推出 tl_bluetooth_audio_sdk(以下简称 SDK),旨在提供统一、高效的音频开发框架,简化不同应用场景下的开发流程,降低音频产品的开发复杂度。

本文档涵盖 SDK 架构与实现、快速上手、应用模块实现、音频通路与算法、Bluetooth 协议栈应用以及各应用方向的项目实现等内容。

SDK 主要特性:

  • 单独 BT 模式支持 2 路连接共存
  • 单 LE 模式支持 4 主 4 从 8 路连接
  • BT/BLE 模式下支持一路 BT 音频加三路 LE ACL 连接
  • BT/BLE 模式下支持一路 LE Audio 加 2 路 BT ACL 连接
  • 支持 BT A2DP In BIS Out 功能
  • 支持作为 BT/BLE Headset 同时连接 BT 手机和 LE Audio 手机,并可动态切换音频播放通路
  • 支持作为 BT/TPSLL Headset 耳机连接一路 TPSLL Dongle, 同时连接一路 BT 手机,进行音乐/电话混音播放
  • 支持 BT/TPSLL TWS 双模在线混音的 TWS 耳机
  • 支持通过 USB 音源的 TPSLL Dongle,经 TPSLL 连接 BT/TPSLL TWS/Headset 耳机
  • 支持通过 USB 音源或 Line In 音源的 BT/BLE Audio Source Dongle,连接 BT/BLE 耳机进行通话和音乐播放
  • SDK 系统支持 FreeRTOS、文件系统、Bootloader、OTA 等基本特性
  • 音频解码支持 SBC、mSBC、AAC(仅解码)、CVSD、LC3/LC3P、OPUS
  • 音频效果算法支持 NN、ANC、VAD、ASRC、ENC、AGC、BF 等

SDK 系统架构

文件目录结构说明

目录结构

  • boot:提供工程编译及连接相关配置
  • common:提供一些通用的跨平台的处理函数,如内存处理函数、字符串处理函数等
  • core: 提供跟芯片平台配置、各模块 log 输出管理、SDK 版本记录等功能
  • drivers:提供与 MCU 紧密相关的硬件设置和外设驱动程序,如 clock、flash、I2C、USB、GPIO、UART 等
  • stack:存放协议栈相关的头文件,源文件被编译到库文件里面,对于用户是不可见的
  • tlkalg:提供加密算法、音频算法相关的函数
  • tlkapi:提供通用的函数,如 flash 数据保存和 FIFO 相关操作的接口
  • tlkapp:封装一些上层应用的调用接口,如音频任务的调度管控、音频 profile 远端状态更新接收处理、及 key 和 LED 的配置等
  • tlklib:存放 SDK 运行所必须的库文件和部分开源代码源码
  • tlkmw:Middle Ware 中间层,各模块功能集成和管理,提供一套更加简洁的交互接口,如 BT 的连接/查询/回连业务、音频播放逻辑等
  • tlksys:存放 SDK 系统相关的一些功能函数,系统任务、timer、PM、双核通信、外设管理、不同芯片平台驱动的 HAL 层等文件
  • vendor:用于存放不同应用项目相关的上层代码

软件架构图

软件架构

  • PHY_RF:IC 用于 RF 收发的物理层
  • BT/BLE/TPSLL Controller:BT/BLE/TPSLL 相关 controller 相关底层链路及逻辑实现
  • BT/BLE/TPSLL HOST:BT/BLE/TPSLL stack 相关 HOST 层逻辑实现
  • BT/BLE/TPSLL Profile:stack 相关 Profile 实现
  • BT/BLE/TPSLL API:上层应用与 stack 之间的中间层
  • SDK System Manager: SDK 相关管理层,包含系统应用、音频管控、UI 承接等
  • Application/UI:SDK 与具体项目相关应用及差异化的 UI 配置
  • AUDIO_Path/Algorithm:音频相关逻辑处理,编解码及额外的算法处理逻辑
  • DSP:针对多核芯片包含有 DSP 模块,承接复杂算法相关的实现
  • CODEC:Codec 模块,承接播放及 MIC 数据采集功能

快速上手

关于 tl_bluetooth_audio_sdk 的快速上手指南,请参阅 tl_bluetooth_audio_sdk Get Started

SDK 系统模块介绍

初始化基本配置

开发板配置

SDK 支持多种开发板配置,用户可以根据实际使用的开发板选择对应的配置。

配置位置:在工程的 app_config.h 文件中,通过 TLKHW_TYPE 宏定义选择开发板,例如:

#define TLKHW_TYPE TLKHW_TLSR9528A_EVK_C1T266A20

添加新的开发:

当需要使用新的开发板时,需要按照以下步骤进行配置:

步骤 1:在 board_config.h 中添加开发板宏定义

进入 vendor/common/boards 目录,编辑 board_config.h 文件,添加新开发板的宏定义。宏定义格式为:TLKHW_<芯片型号>_<开发板型号>,值为一个唯一的十六进制标识符。

示例(添加 C1T266A20 开发板):

#ifndef TLKHW_TLSR9528A_EVK_C1T266A20
    #define TLKHW_TLSR9528A_EVK_C1T266A20 0x266A20
#endif

步骤 2:创建开发板配置文件

vendor/common/boards 目录下,复制一份与目标芯片相同或相似的现有开发板配置文件(例如 B92_C1T266A20.h),重命名为新开发板的配置文件名(建议命名格式:<芯片型号>_<开发板型号>.h)。

说明

  • B92 是指 TLSR952x、TLSR922x 系列芯片。

步骤 3:配置开发板外设

编辑新创建的开发板配置文件,根据实际硬件连接配置以下外设:

  • 按键配置:定义按键的 GPIO 引脚(KEYx_GPIO_INKEYx_GPIO_OUT
  • LED 配置:定义 LED 的 GPIO 引脚和 PWM 通道(GPIO_LED_xxxGPIO_LED_xxx_PWM_ID
  • 串口配置:定义 UART 的 TX/RX 引脚和 DMA 通道(TLKDEV_SERIALx_TX_PINTLKDEV_SERIALx_RX_PIN
  • Codec 配置:定义音频编解码器的相关引脚和 DMA 通道
  • USB 配置:定义 USB 的 DP/DM 引脚(如果支持)
  • 其他外设:根据实际需求配置其他外设的 GPIO

配置文件结构示例:

#if (TLKHW_TYPE == TLKHW_TLSR9528A_EVK_C1T266A20)
    // 按键配置
    #if (TLK_DEV_KEY_ENABLE)
        #define KEY1_GPIO_IN        GPIO_PD6
        #define KEY1_GPIO_OUT       GPIO_PD2
        // ... 其他按键配置
    #endif

    // LED 配置
    #if TLK_DEV_LED_ENABLE
        #define GPIO_LED_BLUE       GPIO_PD0
        // ... 其他 LED 配置
    #endif

    // ... 其他外设配置
#endif

步骤 4:在 default_config.h 中添加开发板配置文件的包含

进入 vendor/common 目录,编辑 default_config.h 文件,在相应的芯片类型分支中添加新开发板配置文件的包含语句。

示例(在 B92 芯片分支中添加):

/*B92*/
#if (TLKHW_TYPE == TLKHW_TLSR9528A_EVK_C1T266A20)
    #include "vendor/common/boards/B92_C1T266A20.h"
#elif (TLKHW_TYPE == TLKHW_YOUR_NEW_BOARD)
    #include "vendor/common/boards/YOUR_NEW_BOARD.h"
// ... 其他开发板配置
#endif

步骤 5:在工程中选择新开发板

在工程的 app_config.h 文件中,将 TLKHW_TYPE 宏定义修改为新添加的开发板宏定义:

#define TLKHW_TYPE TLKHW_YOUR_NEW_BOARD

完成以上步骤后,新开发板配置即可生效。

注意事项:

  • 开发板宏定义的值(十六进制标识符)必须唯一,不能与现有开发板重复。
  • 开发板配置文件名建议使用清晰的命名规范,便于维护。
  • 配置外设时,需要参考芯片数据手册确认 GPIO 引脚的功能和复用关系。
  • 如果新开发板与现有开发板硬件差异较大,建议参考相同芯片型号的其他开发板配置文件作为模板。

系统初始化

系统初始化是 SDK 启动的基础流程,在 main() 函数中通过 tlksys_init() 完成。系统初始化按照以下顺序执行:

(1) 平台初始化 (tlksys_hal_platform_init):供电、主频、Flash 等基础配置

(2) 端口初始化 (tlksys_port_init):GPIO 端口初始化

(3) 操作系统初始化 (tlkos_init):操作系统抽象层初始化

(4) 双核初始化 (tlksys_dualcore_init):双核芯片的核间通信初始化

(5) 定时器初始化 (tlksys_timer_coreInit):系统定时器初始化

(6) 互斥锁创建:创建系统所需的互斥锁

(7) 初始化完成钩子 (tlksys_initFinishedHook):用户自定义的硬件初始化

初始化流程如下:

int main(void)
{
    tlksys_init();                    // 系统初始化
    tlksys_start(tlkapp_create_allTasks);  // 启动系统任务

    // 主循环
    while (1) {
        tlksdk_main_loop();           // 控制器主循环
        tlksys_handler();             // 系统消息处理
    }
    return 0;
}

供电配置:

供电配置通过 tlksys_hal_port_getPlatformInitCfg() 函数返回的配置结构体进行设置。用户需要在工程的 main.c 文件中实现该函数,配置供电相关参数。

配置函数示例:

const tlksys_hal_platform_init_cfg_t *tlksys_hal_port_getPlatformInitCfg(void)
{
    static const tlksys_hal_platform_init_cfg_t cfg = {
        .clockLevel = TLK_CFG_AUDIO_CLOCK_LEVEL,  // 主频配置
        .powerCfg = TLKSYS_HAL_INIT_POWER_CFG_DCDC,  // 供电配置
        // ... 其他配置项
    };
    return &cfg;
}

供电模式选择:

  • TLKSYS_HAL_INIT_POWER_CFG_DEFAULT - 默认供电模式(DCDC)
  • TLKSYS_HAL_INIT_POWER_CFG_DCDC - DCDC 模式,效率高,适合大电流应用
  • TLKSYS_HAL_INIT_POWER_CFG_LDO - LDO 模式,纹波小,适合对电源质量要求高的应用

供电配置说明:

  • DCDC 模式:使用 sys_init(DCDC_1P4_LDO_2P0, ...) 初始化,适合需要高效率的应用场景
  • LDO 模式:使用 sys_init(LDO_1P4_LDO_2P0, ...) 初始化,适合对电源纹波敏感的应用场景

主频设置:

主频配置通过 tlksys_hal_platform_init_cfg_t 结构体中的 clockLevel 字段进行设置。主频设置影响系统性能、功耗和稳定性。

主频配置方式:

(1) 通过宏定义配置:在 app_config.h 中定义 TLK_CFG_AUDIO_CLOCK_LEVEL

#define TLK_CFG_AUDIO_CLOCK_LEVEL 3  // 主频级别

(2) 在平台初始化函数中设置:不同芯片型号的主频设置方式不同

主频级别说明:

  • 不同芯片型号支持的主频级别不同,需要参考对应芯片的 HAL 实现。
  • 主频设置会影响:
    • CPU 运行频率(CCLK)
    • 系统总线频率(HCLK)
    • 外设时钟频率(PCLK)
    • Flash 访问频率(MSPI)

示例(B92 芯片):

// 低功耗模式
clock_init(PLL_CLK_192M, PAD_PLL_DIV, PLL_DIV3_TO_CCLK, 
           CCLK_DIV2_TO_HCLK, HCLK_DIV2_TO_PCLK, PLL_DIV4_TO_MSPI_CLK);
// 结果:CCLK_64M_HCLK_32M_PCLK_16M

// 正常模式
CCLK_96M_HCLK_48M_PCLK_24M;  // CCLK=96MHz, HCLK=48MHz, PCLK=24MHz

注意

  • 主频设置需要与供电电压匹配,高主频通常需要更高的供电电压
  • 低功耗模式下,主频会自动降低以节省功耗
  • Flash 访问频率需要根据实际使用的 Flash 芯片规格进行配置

Controller 工作模式配置

Controller(蓝牙控制器)的工作模式在 tlksys_initFinishedHook() 函数中通过 controller_init() 进行配置。该函数在系统初始化完成后被调用,用于初始化蓝牙相关的硬件和协议栈。

初始化流程:

void tlksys_initFinishedHook(void)
{
#if (BLE_CONTROLLER_INITIAL_EN)
    // (1) RF 模块初始化
    rf_module_init();

    // (2) MCU 硬件初始化
    tlksdk_init_mcu_hardware();

    // (3) Controller 初始化(配置工作模式)
    controller_init(BLE_only, HCI_TR_SOC, NULL, NULL);

    // (4) 调度器初始化
    tlksdk_sch_init();
#endif
}

Controller 工作模式:

controller_init() 函数的第一个参数 mode 用于指定 Controller 的工作模式:

  • BLE_only - 仅支持 BLE(低功耗蓝牙)模式
  • BT_only - 仅支持 BR/EDR(经典蓝牙)模式
  • BT_BLE - 同时支持 BLE 和 BR/EDR 模式
  • BT_TPH - 同时支持 TPH(TPSLL 私有协议控制器)与 BR/EDR(经典蓝牙)模式
  • BT_TPT - 同时支持 TPT(TPSLL 私有协议控制器)与 BR/EDR(经典蓝牙)模式
  • BT_BLE_TPH - 同时支持 TPH(TPSLL 私有协议控制器)、BR/EDR(经典蓝牙)模式、BLE(低功耗蓝牙)模式

HCI 传输模式:

controller_init() 函数的第二个参数 tr_mode 用于指定 HCI 传输模式:

  • HCI_TR_SOC:SoC 模式,Host 和 Controller 在同一芯片内,通过内部接口通信
  • HCI_TR_H4:H4 模式,通过 UART 进行 HCI 通信(需要外部蓝牙芯片)

配置示例:

// BLE 模式,SoC 内部通信
controller_init(BLE_only, HCI_TR_SOC, NULL, NULL);

// BT + BLE 模式,SoC 内部通信
controller_init(BT_BLE, HCI_TR_SOC, NULL, NULL);

// BLE 模式,通过 UART 与外部芯片通信
HCI_TR_UART hci_tr_uart = {
    .tx_pin = GPIO_PC1,
    .rx_pin = GPIO_PC2,
    .baudrate = 115200,
};
controller_init(BLE_only, HCI_TR_H4, &hci_tr_uart, NULL);

注意

  • Controller 初始化必须在 RF 模块和 MCU 硬件初始化之后进行。
  • 不同的工作模式会影响系统功耗和功能支持,使用 H4 模式时需要正确配置 UART 参数(波特率、引脚等)。
  • 某些芯片型号可能不支持所有工作模式。

Key

Key 简介

​在 SDK 的 tlkmw/sys_dev/key 目录下,提供了 key 模块的全部实现。系统上电后,按键模块按“加载、校验、回退/保存”三步完成按键初始化:先实例化按键对象,再从 Flash 读取保存的配置。若配置无效,立即套用默认配置并写入 Flash,确保每次启动都拥有可用且一致的按键设定。

按键初始化流程图

通过 tlkkvdrv_key_insert 接口可向系统注册一个按键实体,其原型声明如下:

/**
 * @brief           Insert a key, initialize its GPIO and start the timer.
 * @param[in]   keyID    - The key identifier, refer to 'TLKDRV_KEY_DID_ENUM'.
 * @param[in]   evtMsk   - A marker for key events, refer to 'TLKDRV_KEY_EVTMSK_ENUM'.
 * @param[in]   inPort   - Input port configuration.
 * @param[in]   outPort  - Output port configuration.
 * @param[in]   level    - Key effective level.
 * @return      TLK_ENONE is success, other value is failure.
                      -TLK_EPARAM  Invalid parameter.
                      -TLK_EREPEAT Key already exists.
                      -TLK_EQUOTA  No more key slots available.
 * @note        Initializes GPIO settings for both simple and matrix keys.
 */
int tlkdrv_key_insert(uint8_t keyID, uint8_t evtMsk, uint16_t inPort, uint16_t outPort, uint8_t level);
typedef enum
{
    TLKDRV_KEY_DID_NONE = 0x0000,
    TLKDRV_KEY_DID_KEY1 = 0x0001,
    TLKDRV_KEY_DID_KEY2 = 0x0002,
    TLKDRV_KEY_DID_KEY3 = 0x0003,
    TLKDRV_KEY_DID_KEY4 = 0x0004,
} TLKDRV_KEY_DID_ENUM;

TLKDRV_KEY_DID_ENUM 用于唯一标识一个按键实体。SDK 默认预留 4 个 ID,若实际按键数量超过 4 个,可继续在 TLKDRV_KEY_DID_ENUM 中顺序扩展。

typedef enum
{
    TLKDRV_KEY_EVTID_LONG,
    TLKDRV_KEY_EVTID_LONG_LONG,
    TLKDRV_KEY_EVTID_CLICK,
    TLKDRV_KEY_EVTID_DCLICK,
    TLKDRV_KEY_EVTID_TCLICK,
    TLKDRV_KEY_EVTID_4CLICK,
    TLKDRV_KEY_EVTID_MAX
} TLKDRV_KEY_EVTID_ENUM;

TLKDRV_KEY_EVTID_ENUM 用于定义按键支持的所有行为类型,包括但不限于:单击、双击、三击、长按等。

typedef enum
{
    TLKDRV_KEY_EVTMSK_NONE      = 0x00,
    TLKDRV_KEY_EVTMSK_LONG      = (1 << TLKDRV_KEY_EVTID_LONG),
    TLKDRV_KEY_EVTMSK_LONG_LONG = (1 << TLKDRV_KEY_EVTID_LONG_LONG),
    TLKDRV_KEY_EVTMSK_CLICK     = (1 << TLKDRV_KEY_EVTID_CLICK),
    TLKDRV_KEY_EVTMSK_DCLICK    = (1 << TLKDRV_KEY_EVTID_DCLICK),
    TLKDRV_KEY_EVTMSK_TCLICK    = (1 << TLKDRV_KEY_EVTID_TCLICK),
    TLKDRV_KEY_EVTMSK_4CLICK    = (1 << TLKDRV_KEY_EVTID_4CLICK), 

    TLKDRV_KEY_EVTMSK_DEFAULT = TLKDRV_KEY_EVTMSK_CLICK | TLKDRV_KEY_EVTMSK_DCLICK | TLKDRV_KEY_EVTMSK_TCLICK | TLKDRV_KEY_EVTMSK_4CLICK |
                                TLKDRV_KEY_EVTMSK_LONG | TLKDRV_KEY_EVTMSK_LONG_LONG,


} TLKDRV_KEY_EVTMSK_ENUM;

TLKDRV_KEY_EVTMSK_ENUM 用于描述按键可被配置的行为集合,默认取值为 TLKDRV_KEY_EVTMSK_DEFAULT。一旦选用该配置,按键即支持该配置下所包含的全部行为。

typedef enum
{
    KEY_EVT_MODE_NONE = 0x00,

    KEY_EVT_MODE_ENABLE_PAIRING_MODE,
    KEY_EVT_MODE_CALL_ACCEPT,
    KEY_EVT_MODE_CALL_HUNG_UP,
    KEY_EVT_MODE_VOLUME_DOWN,
    KEY_EVT_MODE_VOLUME_UP,
    KEY_EVT_MODE_MUSIC_FORWARD,
    KEY_EVT_MODE_MUSIC_BACKWARD,
        ...
    KEY_EVT_MODE_BTTPSLL_MIC_SWITCH,            // debug for tpsll dongle
    KEY_EVT_MODE_SYSTEM_POWEROFF_REQ,
    KEY_EVT_VENDOR_CONFIG,
    KEY_EVT_VENDOR_CONFIG_1   = KEY_EVT_VENDOR_CONFIG,
        ...
    KEY_EVT_VENDOR_CONFIG_END = KEY_EVT_VENDOR_CONFIG_12,

    KEY_EVT_MODE_MAX
} tlkdrv_key_evt_mode_e;

tlkdrv_key_evt_mode_e 用于定义按键行为触发后需要上报的具体事件。例如,可将“KEY1 单击”事件映射为“音乐播放音量增加”。

typedef struct
{
    uint8_t  keyID;
    uint8_t  level;
    uint8_t  isrChn;
    uint8_t  state;
    uint8_t  clickCnt;
    uint8_t  evtMsk;
    uint16_t timeMs;
    uint16_t inPort;
    uint16_t outPort;
} tlkdrv_key_unit_t;

tlkdrv_key_unit_t 完整描述了一个按键实体的静态与动态属性:有效电平、中断通道、实时状态、单击计数、事件掩码、计时器以及输入/输出检测引脚。

typedef struct {
    uint8_t            isNeedScanTimer;
    uint8_t            resv3byte[3];
    uint32_t           lastScanTick;
    TlkOsTimerHandle_t scanTimer;
    tlkdrv_key_unit_t  unit[TLKDRV_KEY_MAX_NUMB];
} tlkdrv_key_ctrl_t;

tlkdrv_key_ctrl_t 为按键模块全局控制块,集中管理扫描策略、节拍记录、定时器资源以及所有按键实体的运行时数据。

Key 使用

按键触发后,由驱动层采集并解析为 keyIDevtID。状态机完成有效性校验,再依据消息映射表将指令分发至相应任务,实现闭环式按键响应。

按键使用流程图

API 介绍

typedef void (*tlkdrv_vendor_config_cb_t)(void);

tlkdrv_key_evt_mode_e 除标准事件外的其他自定义事件(KEY_EVT_VENDOR_CONFIG)产生后的回调函数。用户在该函数中处理一些逻辑,不需要通过 SDK 消息分发机制到相应 task 运行指定的函数。

/**
 * @brief       Register a vendor configuration callback function.
 * @param[in]   eventMode  - Event mode identifier (KEY_EVT_VENDOR_CONFIG_x).
 * @param[in]   cb         - Callback function to register.
 * @return      none.
 * @note        Only registers callback if eventMode is within valid range.
 */
void tlkdrv_key_registerVendorConfigCallback(uint8_t eventMode, tlkdrv_vendor_config_cb_t cb);

自定义事件 KEY_EVT_VENDOR_CONFIG_X 对应的事件处理函数,被其他 tlkdrv_key_registerVendorConfigXCallback 调用。

/**
 * @brief       Register a vendor configuration callback for event mode KEY_EVT_VENDOR_CONFIG_1.
 * @param[in]   cb  - Callback function to register.
 * @return      none.
 * @note        Wrapper for tlkdrv_key_registerVendorConfigCallback with fixed event mode.
 */
void tlkdrv_key_registerVendorConfig1Callback(tlkdrv_vendor_config_cb_t cb)
{
    tlkdrv_key_registerVendorConfigCallback(KEY_EVT_VENDOR_CONFIG_1, cb);
}

自定义事件 KEY_EVT_VENDOR_CONFIG_X 对应的回调注册函数。

LED

LED 模块介绍

LED 在 SDK 中起指示作用,作为状态指示,可以直观的反馈设备当前处于哪种状态。LED 接口设计主要为用户提供统一的 LED 接口,方便用户直接使用。LED 接口提供了普通 IO 和 PWM 两种类型的接口分别实现 LED 的亮、灭、闪烁和呼吸效果,能满足用户对 LED 的各种使用场景。

LED 使用流程

LED 初始化:

LED 的初始化只是在系统函数里面初始化,在使能 RTOS 时,LED 线程挂在系统线程下面。在使用 LED 模块时,需要打开模块的使能宏定义:

#define TLK_DEV_LED_ENABLE 1

初始化的接口为:

void tlkapp_sysLed_init(void);

LED 配置:

LED 可以通过宏定义配置 GPIO 和 PWM,具体的配置宏定义如下:

#define GPIO_LED_BLUE        GPIO_PB3
#define GPIO_LED_BLUE_PWM_ID PWM0_ID
#define GPIO_LED_RED         GPIO_PB4
#define GPIO_LED_RED_PWM_ID  PWM1_ID
#define GPIO_LED_NUMS        2
#define LED_ON_LEVEL         1 

以上的配置位置在 SDK 的vendor/common/board目录下的各个硬件对应的 *.h 文件中。

如果 GPIO_LED_XXXX_PWM_ID 的值为合法的 PWMx_ID,则 LED 的驱动方式默认会选择为 PWM 驱动,否则为 GPIO 驱动。在不同的状态下,LED 的闪烁效果是不一样,可以根据需要选择不同的闪烁效果。默认的配置表保存在tlkdrv_led_patterns数组中。

typedef struct
{
    uint16_t behavior;
    uint16_t stepUs;
    uint16_t dutyFlushMs;
    uint16_t onTimerMs;
    uint16_t offTimerMs;
    uint16_t flashCounts;
}tlkdrv_led_pattern_t;
  • behavior:表示 LED 的状态,0:常灭,1:常亮,2:闪烁,3:呼吸
  • stepUs:调整 PWM 的步长,单位:us
  • dutyFlushMs:调整 PWM 的时间间隔,单位:ms
  • onTimerMs:LED 亮的时间,单位: ms
  • offTimerMs:LED 灭的时间,单位:ms
  • flashCounts:LED 闪烁的次数

tlkdrv_led_pattern_t为 LED 的配置结构体,包含 LED 的状态、步长、时间间隔、亮灭时间、闪烁次数等信息。SDK 中提供了默认的配置表,用户可以根据需要添加新的配置,并添加到tlkdrv_led_patterns数组中。

static const tlkdrv_led_pattern_t tlkdrv_led_patterns[TLKDRV_LED_PATTERN_MAX_STATE] = {
    [TLKDRV_LED_PATTERN_OFF]                 = {.behavior = 0, .stepUs = 0,   .dutyFlushMs = 0,   .onTimerMs = 0,    .offTimerMs = 0,    .flashCounts = 0xFFFFU},
    [TLKDRV_LED_PATTERN_ON]                  = {.behavior = 1, .stepUs = 0,   .dutyFlushMs = 0,   .onTimerMs = 0,    .offTimerMs = 0,    .flashCounts = 0xFFFFU},
    [TLKDRV_LED_PATTERN_FLASH_SLOW]          = {.behavior = 2, .stepUs = 0,   .dutyFlushMs = 0,   .onTimerMs = 2000, .offTimerMs = 2000, .flashCounts = 0xFFFFU},
    [TLKDRV_LED_PATTERN_FLASH_FAST]          = {.behavior = 2, .stepUs = 0,   .dutyFlushMs = 0,   .onTimerMs = 200,  .offTimerMs = 200,  .flashCounts = 0xFFFFU},
    [TLKDRV_LED_PATTERN_FLASH_PAIR]          = {.behavior = 2, .stepUs = 0,   .dutyFlushMs = 0,   .onTimerMs = 500,  .offTimerMs = 1500, .flashCounts = 0xFFFFU},
    [TLKDRV_LED_PATTERN_IDLE]                = {.behavior = 2, .stepUs = 0,   .dutyFlushMs = 0,   .onTimerMs = 200,  .offTimerMs = 800,  .flashCounts = 0xFFFFU},

    [TLKDRV_LED_PATTERN_BREATH_SLOW]         = {.behavior = 3, .stepUs = 25,  .dutyFlushMs = 10,  .onTimerMs = 2000, .offTimerMs = 2000, .flashCounts = 0xFFFFU},
    [TLKDRV_LED_PATTERN_BREATH_FAST]         = {.behavior = 3, .stepUs = 125, .dutyFlushMs = 5,   .onTimerMs = 200,  .offTimerMs = 200,  .flashCounts = 0xFFFFU},
    [TLKDRV_LED_PATTERN_BREATH_PAIR]         = {.behavior = 3, .stepUs = 50,  .dutyFlushMs = 5,   .onTimerMs = 500,  .offTimerMs = 500,  .flashCounts = 0xFFFFU},

};

在实际使用过程中,用户可以在tlkapp_sysLed_stateToPatternHook函数里配置,该函数为虚函数,用户可以自己定义。如果用户不定义,则 SDK 会使用默认的配置。下面为 SDK 的默认配置:

__attribute__((weak)) tlkdrv_led_pattern_e tlkapp_sysLed_stateToPatternHook(TLKAPP_LED_STATE state)
{
    static const tlkdrv_led_pattern_e sTlkapp_sysLed_defaultCfg[TLKAPP_LED_STATE_NUM] = {
        [TLKAPP_LED_STATE_POWERON]      = TLKDRV_LED_PATTERN_ON_1S,       
        [TLKAPP_LED_STATE_POWEROFF]     = TLKDRV_LED_PATTERN_ON_1S,          
        [TLKAPP_LED_STATE_POWERON_IDLE] = TLKDRV_LED_PATTERN_IDLE,       
        [TLKAPP_LED_STATE_PARING]       = TLKDRV_LED_PATTERN_FLASH_FAST,  
        [TLKAPP_LED_STATE_CONNECTED]    = TLKDRV_LED_PATTERN_BREATH_SLOW,
        [TLKAPP_LED_STATE_LOWBATTARY]   = TLKDRV_LED_PATTERN_SLOW_FLASH_3_TIMES,
    };
    return sTlkapp_sysLed_defaultCfg[state];
}

LED 的调用

在 SDK 中,LED 的调用为事件驱动型,调用的统一接口为:tlkapp_sysUI_updateHandleState。该接口会根据当前的状态,调用 LED 模块的接口来实现 LED 的状态切换。

int tlkapp_sysUI_updateHandleState(uint08 group, uint16 handle, uint08 state)
{
    tlksys_mutex_lock(TLKSYS_MUTEX_UI);
    int res = tlkapp_sysUI_updateHandleStateCore(group, handle, state);
    tlksys_mutex_unlock(TLKSYS_MUTEX_UI);
    return res;
}
  • group: 组号,代表是哪个组的事件,有以下选择。
typedef enum
{
    TLKAPP_UI_HANDLE_GROUP_SYS,
    TLKAPP_UI_HANDLE_GROUP_BT,
    TLKAPP_UI_HANDLE_GROUP_BLE,
    TLKAPP_UI_HANDLE_GROUP_TPSLL,
    TLKAPP_UI_HANDLE_GROUP_MAX,
} TLKAPP_UI_HANDLE_GROUP_ENUM;
  • handle: 句柄号,代表是在哪个链路触发的事件。
  • state: 状态,代表要设置哪种状态下的灯效。

最终会调用下面的接口置的函数来实现 LED 的切换:

int tlkapp_sysLed_funAction(TLKAPP_LED_STATE led_fun);
typedef enum
{
    TLKAPP_LED_STATE_POWERON,
    TLKAPP_LED_STATE_POWEROFF,
    TLKAPP_LED_STATE_POWERON_IDLE,
    TLKAPP_LED_STATE_PARING,
    TLKAPP_LED_STATE_CONNECTED,
    TLKAPP_LED_STATE_LOWBATTARY,
    TLKAPP_LED_STATE_NUM,
} TLKAPP_LED_STATE;

TLKAPP_LED_STATE 表示了当前处于系统处何种状态。

使用流程如下:

LED

UART

UART 模块配置

UART 模块初始化:

在使用 UART 模块时,首先要对该模块进行初始化,需要调用以下函数:

int tlkdrv_serial_mount(uint8_t port, uint32_t baudRate, uint16_t txPin, uint16_t rxPin, uint8_t txDma, uint8_t rxDma);
  • port: 串口号,对应 UART0~UART1
  • baudRate: 波特率
  • txPin: 串口发送引脚
  • rxPin: 串口接收引脚
  • txDma: 串口发送 DMA,如果使用 UART 的 DMA 方式发送数据,该参数配置发送 DMA 的通道号;不使用 DMA 发送时直接写 0;
  • rxDma: 串口接收 DMA

数据缓冲区配置:

SDK 的 UART 模块提供了数据缓冲区,用户可以根据自己的需求配置,默认的配置如下。

#define TLKMDI_COMM_SERIAL_RBUFF_NUMB 2
#define TLKMDI_COMM_SERIAL_RBUFF_SIZE 540
#define TLKMDI_COMM_SERIAL_SBUFF_NUMB 4
#define TLKMDI_COMM_SERIAL_SBUFF_SIZE 128
  • TLKMDI_COMM_SERIAL_RBUFF_NUMB: 接收缓冲区的个数
  • TLKMDI_COMM_SERIAL_RBUFF_SIZE: 接收缓冲区的大小
  • TLKMDI_COMM_SERIAL_SBUFF_NUMB: 发送缓冲区的个数
  • TLKMDI_COMM_SERIAL_SBUFF_SIZE: 发送缓冲区的大小

发送缓冲区配置:

int tlkdrv_serial_setTxQFifo(uint8_t port, uint16_t fnumb, uint16_t fsize, uint8_t *pBuffer, uint32_t buffLen);
  • port: 串口号,对应 UART0~UART1
  • fnumb: 发送缓冲区的个数
  • fsize: 发送缓冲区的大小
  • pBuffer: 发送缓冲区的起始地址
  • buffLen: 发送缓冲区的长度

接收缓冲区配置:

int tlkdrv_serial_setRxQFifo(uint8_t port, uint16_t fnumb, uint16_t fsize, uint8_t *pBuffer, uint32_t buffLen);
  • port: 串口号,对应 UART0~UART1
  • fnumb: 接收缓冲区的个数
  • fsize: 接收缓冲区的大小
  • pBuffer: 接收缓冲区的起始地址
  • buffLen: 接收缓冲区的长度

UART 模块使能:

在使用 UART 模块时,首先要对该模块进行使能,需要在系统初始化的时候调用以下函数:

int tlkdrv_serial_open(uint8_t port);
  • port: 串口号,对应 UART0~UART1

UART 模块发送接口:

通过tlkdrv_serial_write接口,用户可以向指定的串口发送数据。

int tlkdrv_serial_send(uint8_t port, uint8_t *pData, uint16_t dataLen);
  • port: 串口号,对应 UART0~UART1
  • pData: 待发送的数据指针
  • dataLen: 待发送的数据长度

UART 模块接收接口:

在接收 UART 数据时,需要先注册接收函数入口,使用下面接口进行注册:

void tlkdrv_serial_regCB(uint8_t port, TlkDrvSerialRecvCB cb);
  • port: 串口号,对应 UART0~UART1
  • cb: 接收回调函数,当接收到数据时,会调用该函数,并将接收到的数据作为参数传入。

UART 中断注册:

SDK 的 UART 模块提供了中断注册接口,用户可以根据自己的需求进行注册。

LIC_ISR_REGISTER_OS(tlk_uartx_irq_handler, IRQ_UARTx)

如果接收数据使用 DMA 方式,需要注册 DMA 的中断,并在中断中调用tlkdrv_serial_dma_irq_handler

PLIC_ISR_REGISTER_OS(tlk_dma_irq_handler, IRQ_DMA)

提示音

在 SDK 中,提示音模块主要用于提示音效,比如连接成功、连接断开、搜索设备等。SDK 提供了两种提示音的播放格式,分别是ADPCMSBC格式,用户可以根据需要选择,下载的提示音文件必须是对应的格式(只支持单一格式,不支持混合格式)。

提示音配置

在使用提示音功能时,必须在对应工程的 app_config.h 文件中使能宏定义 TLK_CFG_TONE_ENABLE

#define TLK_CFG_TONE_ENABLE 1

如果下载的提示音文件是 SBC 编码格式的,必须在 app_config.h 文件中打开宏定义TONE_SBC_EN

#define TONE_SBC_EN 1

提示音初始化

提示音的初始化依赖于音频任务,需要在系统初始化的时候初始化音频任务。

#if (TLK_MW_AUDIO_ENABLE)
    tlksys_task_create(TLKSYS_TASKID_AUDIO, tlkapp_audio_getTaskCfg());
#endif

提示音下载位置

在使用提示音功能时,首先需要将编译生成的提示音文件下载到 Flash 的某个位置,具体地址定义详见提示音下载章节的描述。提示音的相关配置如下:

tone_cfg_t g_tone_cfg = {
                        .volume = 512,
                    #if TONE_SBC_EN
                        .type   = TONE_TYPE_SBC,
                    #else
                        .type   = TONE_TYPE_ADPCM,
                    #endif
                         .busy   = 0,
                         .ready  = 0,
                         .buff   = NULL};

提示音播放接口

提示音的编码格式有两种,但是播放的流程是一样的。在播放提示音时,需要调用 tone_play 接口,该接口会根据配置的提示音类型,播放对应的提示音文件。但是在 SDK 中,通常不会直接调用该接口。由于提示音播放采用异步机制,需要通过事件触发。在 SDK 中,使用tlkapp_sysUI_updateHandleState()接口去触发相关的 UI,如果有提示音需要播放,则会通过tlkapp_sysUI_sendStartToneMsg()通知 UI 去播放提示音。

static void tlkapp_sysUI_sendStartToneMsg(uint08 tone_indx)
{
    (void)tone_indx;
    uint08 data[2] = {tone_indx,1};
    tlksys_sendMsg(TLKSYS_TASKID_AUDIO, TLKSYS_AUD_MSGID_START_TONE_CMD, data, 2);
}

TLKSYS_AUD_MSGID_START_TONE_CMD事件最终会触发int tlkmdi_tone_start(uint16 handle, uint32 param)接口,该接口会根据配置的提示音类型,播放对应的提示音文件。

int tlkmdi_tone_start(uint16 handle, uint32 param)

USB

USB 相关目录介绍

Telink USB 支持标准的 USB1.1、USB2.0 协议,具体支持协议类型可参考 core spec。

Telink USB 模块采用外部供电,目前 SDK 支持的设备类包括 UDB、MSC、UAC,用户可通过修改 TLK_CFG_USB_ENABLE 宏来使能或禁用 USB 模块, 修改 TLK_USB_XXX_ENABLE 宏来改变对应设备类的可用性。

下图为 USB 模块相关的文件结构:

目录结构

  • tlkusb.*: USB 模块使能/禁用、应用模式切换、枚举流程处理等相关接口
  • tlkusb_struct.h: USB 模块相关结构体定义
  • tlkusb_msg.*: USB 模块 DCD 消息处理相关接口,目前仅使用于 High Speed USB 模块
  • tlkusb_hal.*: USB 模块底层 HAL 接口,目前适配了多款芯片的 USB1.1 模块
  • tlkusb_desc.*: USB 模块应用描述符相关接口
  • tlkusb_define.h: USB 模块相关宏定义
  • tlkusb_core.*: USB 模块核心处理相关接口,包括初始化、枚举、控制等
  • tlkusb_module.*: USB 模块抽象的接口,包括初始化、枚举、控制等
  • msc/uac/uac_hs/udb: USB 模块的应用层接口及数据处理,对应 TLKHAL_USB_MODE_ENUM,目前 SDK 仅适配了这四种模式。

USB 接口使用

USB 模块初始化

int tlkusb_init(uint08 index, uint16 usbID);
  • index: USB 的 index,仅适用于支持多个 USB 模块的系统
  • usbID: UDB 模式下与 USB 上位机对应的 USB ID,其他模式下此 ID 暂无实际意义

USB 模块打开接口

int tlkusb_open(uint08 index, TLKHAL_USB_MODE_ENUM modType);
  • index: USB 的 index, 仅适用于支持多个 USB 模块的系统
  • modType: 设备的类型,对应 TLKHAL_USB_MODE_ENUM

使能 USB 事件触发机制,使能 USB 枚举需要的相关中断,并配置相关的中断 mask。若枚举流程为 loop 触发,则不需要调用此接口。

void tlkusb_hal_enable_eventMode(void);

USB 模块对应的 loop handler 的挂载与注册,目前 SDK 的 USB 模块是挂载到 TLKSYS_TASKID_SYSTEM 线程下运行。

tlksys_task_regEvtCB(TLKSYS_TASKID_SYSTEM,TLKSYS_TASK_EVT_SYS_USB,tlkusb_handler);

USB 枚举流程介绍

SDK 当前采用中断触发机制实现 USB 枚举。需要调用 tlkusb_hal_enable_eventMode() 接口来使能 USB 模块相关 irq,并在 irq 中处理 USB 枚举事件。下面以 B92 平台的 UDB 设备为例,结合代码,介绍下 USB 枚举流程。

(1) 设备初始化

tlksys_task_regEvtCB(TLKSYS_TASKID_SYSTEM,TLKSYS_TASK_EVT_SYS_USB,tlkusb_handler);
tlkusb_init(TLK_CFG_USB_UDB_INDEX, 0x120);
tlkusb_open(TLK_CFG_USB_UDB_INDEX, TLKUSB_MODTYPE_UDB);
tlkusb_hal_enable_eventMode();

(2) 中断服务函数注册及处理

tlkusb_ctrl_ep_irq_handler 作用是获取 USB 的 irq mask,并记录到全局的 sTlkUsbReg.ctrlEpIrq[index] 中,清除对应的 USB irq mask。此处的 index 代表了 USB 的 index。随后置位 System 中的 USB bit,触发对应注册的 USB loop handler。后续的枚举流程在 USB 的 Loop Handler 中进行处理。

_attribute_ram_code_sec_ void tlk_usb_ctrl_ep_irq_handler(void)
{
    tlkusb_ctrl_ep_irq_handler(0);
}
PLIC_ISR_REGISTER_OS(tlk_usb_ctrl_ep_irq_handler, IRQ_USB_CTRL_EP_SETUP)
PLIC_ISR_REGISTER_OS(tlk_usb_ctrl_ep_irq_handler, IRQ_USB_CTRL_EP_DATA)
PLIC_ISR_REGISTER_OS(tlk_usb_ctrl_ep_irq_handler, IRQ_USB_CTRL_EP_STATUS)
PLIC_ISR_REGISTER_OS(tlk_usb_ctrl_ep_irq_handler, IRQ_USB_RESET)
PLIC_ISR_REGISTER_OS(tlk_usb_ctrl_ep_irq_handler, IRQ_USB_CTRL_EP_SETINF)

(3) USB Loop Handler

USB 枚举的核心处理主要包括枚举过程中 Setup 和 Data 等数据的处理,以及描述符的返回。

void tlkusb_handler(void);

USB 设备类型

(1) TLKUSB_MODTYPE_UDB

USB Debug Class 设备,用于调试,搭配 USB 上位机使用。相关描述符详见 tlkusb_udbDesc.c。

端点资源:

端点资源使用如下,不同芯片间会有差异,客户可自行修改:

    #define TLKUSB_UDB_EDP_DBG_IN  USB_EDP3_IN
    #define TLKUSB_UDB_EDP_DBG_OUT USB_EDP5_OUT
    #define TLKUSB_UDB_EDP_VCD_IN  USB_EDP8_IN
    #define TLKUSB_UDB_EDP_VCD_OUT USB_EDP6_OUT

数据处理接口:

读取 TLKUSB_UDB_EDP_DBG_OUT 端点数据,解析是 USB download 命令还是 USB shell 命令。

static void tlkusb_udbctrl_handler(void);

USB shell 命令处理, 目前是弱定义,客户可重定义。

void tlkusb_debug_shell_hook(uint8_t *pData, uint16_t dataLen);

(2) TLKUSB_MODTYPE_UAC

USB Audio Class,USB 音频设备,相关描述符详见 tlkusb_uacDesc.c。

端点资源:

端点资源使用如下,不同芯片间会有差异,客户可自行修改:

    #define TLKUSB_UAC_EDP_HID USB_EDP1_IN
    #define TLKUSB_UAC_EDP_MIC USB_EDP7_IN
    #define TLKUSB_UAC_EDP_SPK USB_EDP6_OUT

数据处理接口:

void tlkusb_uacirq_handler(void);
void tlkusb_uacspk_recvData(uint32 tick);
void tlkusb_uacmic_fillData(uint32 tick);

(3) TLKUSB_MODTYPE_MSC

USB Mass Storage Class,USB 存储设备,相关描述符详见 tlkusb_mscDesc.c

端点资源:

端点资源使用如下,不同芯片间会有差异,客户可自行修改:

    typedef enum{
        TLKUSB_MSC_EDP_IN  = 1, //USB_EDP1_IN
        TLKUSB_MSC_EDP_OUT = 5, //USB_EDP5_OUT
    }TLKUSB_MSC_EDP_ENUM;

数据处理接口:

void tlkusb_mscctrl_handler(void);

定时器

本章将详细介绍 SDK 中的定时器系统架构。定时器包括三种:系统线程定时器(tlksys_timer)、操作系统抽象层定时器(tlkos_timer)以及硬件定时器。

timerList 介绍和实现原理

tlkapi_timerList 是 SDK 中的一个定时器底层管理模块,提供了创建、启动、停止、销毁定时器以及处理定时器超时事件的功能。它采用了循环链表数据结构来组织定时器,并根据定时器的到期时间进行排序,以便高效地管理和调度多个定时器。系统线程定时器(tlksys_timer)、操作系统抽象层定时器(裸机)底层采用该模块对各个定时器进行集中统一管理调度。用户可不关心该模块的具体原理和实现,直接使用 tlksys_timer、tlkos_timer 的相关接口即可。

关键特性:

  • 循环链表管理:所有定时器通过循环链表组织,按到期时间排序
  • 状态机管理:定时器具有完整生命周期状态管理
  • 自动重载支持:支持单次触发和周期性触发模式
  • 异步安全调度:在回调执行期间的操作(如停止、重启、销毁)不会立即执行,而是设置相应状态标记,等待回调完成后处理

(1) 定时器节点结构

typedef struct TlkApiTimer_s {
    uint8_t               malloced;     // 是否动态分配
    uint8_t               runningCB;    // 回调函数是否正在执行
    uint8_t               autoReload;   // 自动重载标志
    uint8_t               nowState;     // 当前状态
    uint32_t              arrival;      // 到期时间戳
    uint32_t              timeout;      // 超时时间
    void*                 userArg;      // 用户参数
    TlkApiTimerCB_t       timerCB;      // 回调函数
    struct TlkApiTimer_s *pNext;        // 下一个节点指针
} TlkApiTimer_t;

(2) 定时器列表控制结构

typedef struct{    
    TlkApiTimer_t *pList;  // 指向第一个定时器节点
} TlkApiTimerList_t;

定时器节点具有四种状态,通过状态机进行管理,状态转换如下图所示。当定时器启动时,该节点插入管理链表。管理链表基于超时时间升序排布各节点,当节点到达超时时间从链表中删除该节点并执行对应的回调函数。如果该节点设置了自动重装载,则再次插入回链表进行重新记时。

enum {
    TLKAPI_TIMER_STATE_NONE = 0,    // 初始状态
    TLKAPI_TIMER_STATE_START,       // 运行状态
    TLKAPI_TIMER_STATE_STOP,        // 停止状态
    TLKAPI_TIMER_STATE_DELETE,      // 待删除状态
};

Timer State Machine

节点管理链表,按超时时间升序排布。

timer list structure

(3) 相关 API 介绍

函数表速查:

功能 函数 复杂度
创建静态 tlkapi_timer_createStatic O(1)
创建动态 tlkapi_timer_create O(1) + malloc
启动 tlkapi_timer_start O(n) 插入
重启 tlkapi_timer_reStart O(n) 删除 + 插入
停止 tlkapi_timer_stop O(n) 删除
销毁 tlkapi_timer_destroy O(n) 删除 + free/清零
取最近到期时间 tlkapi_timerList_getNextTimeUs O(1)
主循环处理 tlkapi_timerList_handler O(k) k=到期个数

定时器创建

tlkapi_timer_createStatic: 静态创建定时器,使用用户提供缓冲区。

/**
 * @brief           Statically create a timer (using provided buffer)
 * @param[in]       buffer - Pointer to TlkApiTimer_t structure to hold timer information
 * @param[in]       periodUs - Timer period (microseconds)
 * @param[in]       autoReload - Whether to automatically reload (non-zero for auto-reload)
 * @param[in]       CBEnter - Callback function to call when timer expires
 * @param[in]       usrArg - User argument to pass to callback function
 * @return          TLK_ENONE for success, other negative values for failure
 * @note            This function does not allocate memory, directly uses the passed buffer as timer storage space
 */
int32_t tlkapi_timer_createStatic(TlkApiTimer_t *buffer, uint32_t periodUs, uint32_t autoReload, TlkApiTimerCB_t CBEnter, void *usrArg);

tlkapi_timer_create: 动态创建定时器,内部自动分配内存。

/**
 * @brief           Dynamically create a timer (allocates memory internally)
 * @param[out]      timerHandle - Pointer to store the created timer handle
 * @param[in]       periodUs - Timer period (microseconds)
 * @param[in]       autoReload - Whether to automatically reload (non-zero for auto-reload)
 * @param[in]       CBEnter - Callback function to call when timer expires
 * @param[in]       usrArg - User argument to pass to callback function
 * @return          TLK_ENONE for success, other negative values for failure
 * @note            This function allocates memory internally, need to call tlkapi_timer_destroy to release
 */
int32_t tlkapi_timer_create(TlkApiTimerHandle_t *timerHandle, uint32_t periodUs, uint32_t autoReload, TlkApiTimerCB_t CBEnter, void *usrArg);

两种创建方式的区别在于内存管理方式,前者由用户负责内存分配,后者由系统自动分配并在销毁时释放。

定时器控制

tlkapi_timer_start: 启动定时器。

/**
 * @brief           Start a timer
 * @param[in]       list - Timer list pointer
 * @param[in]       timerHandle - Timer handle to start
 * @return          TLK_ENONE for success, other negative values for failure
 */
int32_t tlkapi_timer_start(TlkApiTimerList_t *list, TlkApiTimerHandle_t timerHandle);

tlkapi_timer_reStart: 重启定时器(如果已在运行则重新计时)。

/**
 * @brief           Restart a timer
 * @param[in]       list - Timer list pointer
 * @param[in]       timerHandle - Timer handle to restart
 * @return          TLK_ENONE for success, other negative values for failure
 * @note            If the timer is already running, it will be stopped and restarted
 */
int32_t tlkapi_timer_reStart(TlkApiTimerList_t *list, TlkApiTimerHandle_t timerHandle);

tlkapi_timer_stop: 停止定时器。

/**
 * @brief           Stop a timer
 * @param[in]       list - Timer list pointer
 * @param[in]       timerHandle - Timer handle to stop
 * @return          TLK_ENONE for success, other negative values for failure
 * @note            If the timer is executing its callback function, it will be automatically stopped after the callback finishes
 */
int32_t tlkapi_timer_stop(TlkApiTimerList_t *list, TlkApiTimerHandle_t timerHandle);

tlkapi_timer_destroy: 销毁定时器。

/**
 * @brief           Destroy a timer
 * @param[in]       list - Timer list pointer
 * @param[in]       timerHandle - Timer handle to destroy
 * @return          TLK_ENONE for success, other negative values for failure
 * @note            If the timer is executing its callback function, it will be automatically destroyed after the callback finishes
 */
int32_t tlkapi_timer_destroy(TlkApiTimerList_t *list, TlkApiTimerHandle_t timerHandle);

tlkapi_timer_setPeriod: 设置定时器周期。

/**
 * @brief           Set timer period
 * @param[in]       list - Timer list pointer
 * @param[in]       timerHandle - Timer handle to set
 * @param[in]       periodUs - New timer period (microseconds)
 * @return          TLK_ENONE for success, other negative values for failure
 * @note            If the timer is running, it will reschedule the next expiration time
 */
int32_t tlkapi_timer_setPeriod(TlkApiTimerList_t *list, TlkApiTimerHandle_t timerHandle, uint32_t periodUs);

tlkapi_timer_isStarted: 查询定时器是否已启动。

/**
 * @brief           Check if timer is started
 * @param[in]       timerHandle - Timer handle to check
 * @return          true if timer is started, false otherwise
 */
bool tlkapi_timer_isStarted(TlkApiTimerHandle_t timerHandle);

定时器处理

tlkapi_timerList_handler: 处理超时的定时器,调用相应的回调函数。

/**
 * @brief           Process expired timers
 * @param[in]       list - Timer list pointer
 * @note            This function will call the callback functions of all expired timers
 */
void tlkapi_timerList_handler(TlkApiTimerList_t *list);

tlkapi_timerList_getNextTimeUs: 获取到下一个定时器超时的时间间隔。

/**
 * @brief           Get time until next timer expires (in microseconds)
 * @param[in]       list - Timer list pointer
 * @return          Time until next timer expires (in microseconds), or TLKOS_WAIT_FOREVER if no timers
 */
uint32_t tlkapi_timerList_getNextTimeUs(TlkApiTimerList_t *list);

(4) 使用注意事项

  • 最小定时器时间约为 50 微秒(TLKAPI_TIMEOUT_MIN)
  • 最大定时器时间约为 67 秒(TLKAPI_TIMEOUT_MAX)
  • 回调函数中不建议执行耗时操作,以免影响其他定时器处理
  • 定时器回调函数中可以安全地调用定时器控制函数(如启动、停止、销毁等)
  • 使用动态创建的定时器必须调用 tlkapi_timer_destroy 进行资源释放

tlksys_timer 线程/任务定时器

tlksys_timer 是 SDK 的线程/任务级定时器管理模块,它基于 tlkapi 层的 tlkapi_timerList 模块构建,为系统中的各个任务/线程提供定时器功能。该模块通过任务 ID 将定时器与特定任务关联,使得每个任务可以独立管理自己的定时器集合,相关 API 见 tlksys_timer.h。该类定时器仅用以进行粗略的周期任务场景,如需更高精度/优先级的定时器可以使用 tlkos_timer 或直接使用外设硬件定时器。

基本原理:

每个任务都拥有自己的定时器列表。任务结构体 tlksys_task_t 包含一个 timerList 字段:

typedef struct {
    uint16_t taskID;
    uint16_t state;
    TlkApiTimerList_t timerList;  // 每个任务的定时器列表
    const tlksys_task_cfg_t *pCfgs;
    TlkOsTaskHandle_t taskHandle;
} tlksys_task_t;

RTOS 下,在每个任务的主循环中,系统会调用 tlkapi_timerList_handler 来处理挂在该任务上的定时器,执行已超时定时器的回调函数。

当所有业务完成后通过 tlkapi_timerList_getNextTimeUs 函数获取线程需要阻塞的时间,使用 tlksys_task_waitEvent 等待下一个唤醒周期。

相关说明:

  • tlksys_task_waitEvent 函数的作用是让线程进入阻塞态,线程可被已订阅的事件(信号量)提前唤醒进入就绪态,或超时时间到期时被唤醒。
  • 如果该线程上暂时没有已经启动的定时器,则阻塞时间为 TLKOS_WAIT_FOREVER,仅事件可唤醒线程。
  • 定时器的处理 handler 调度基于 RTOS 的阻塞延时即调度框架,若有优先级更高的线程在运行,即使定时器已经超时,也会暂时被挂起,直到本线程获得时间片。
  • 对任务的定时器链表进行操作后,会调用 tlksys_task_setEvt 来唤醒对应的线程,用以更新线程的阻塞时间。
static void tlksys_template_task(void *arg)
{
    //初始化代码
    while(1){
        // ... 其他处理
        tlkapi_timerList_handler(&task->timerList);  // 处理定时器超时
        // ... 其他处理
        uint32_t blockTimeMs = tlkos_task_nextIntvUsToMs(tlkapi_timerList_getNextTimeUs(&task->timerList),1,TLKOS_WAIT_FOREVER);
        tlksys_task_waitEvent(task->taskID,blockTimeMs);  
    }
}

裸机下,所有任务运行在一个循环中。每个任务轮询各自注册的事件以及处理定时器 handler。

当暂无事件需要处理,则进入 idle task,其会获取所有任务最近的超时时间的最小值,并将该时间点设置为下一个唤醒点,以进行低功耗行为(WFI/suspend)。

void tlksys_task_handler(void)
{
    for (size_t index = 0; index < TLKSYS_TASKID_MAXNUM; index++) {
        tlksys_task_t *pTask = &sTlkSysTaskList[index];
        //... 逻辑已精简
        tlkos_event_wait(sTlkTaskEvtTabHandles[index],0);
        tlkapi_timerList_handler(&pTask->timerList); 
        //... 逻辑已精简
    }
    tlksys_task_idleTask(); //idle task用以低功耗
}

RTOS 下的线程安全机制:

tlksys_timer 模块通过互斥锁保证线程安全:

  • 每个任务都有一个对应的互斥锁
  • 在操作定时器前获取互斥锁
  • 操作完成后释放互斥锁
  • 这确保了在多线程环境中对同一任务的定时器进行并发操作时的数据一致性。

使用示例:

// 假设在SYSTEM线程中使用定时器
static TlkApiTimer_t sMyTimer;

static void myTimerCB(TlkApiTimerHandle_t handle, void* userArg)
{
    // 处理定时器事件
    // 可以在这里执行任务特定的操作
    (void) handle;
    (void) userArg;
    //1s周期定时器,自动重装载
    tlk_printf("print log per 1s");
    if(something_happened){
        //伪代码,当某些ui发生时,停止定时器
        tlksys_timer_stop(TLKSYS_TASKID_SYSTEM, &sMyTimer);
    }
}

// 在任务初始化时创建定时器
void user_demo_init(void)
{
    tlksys_timer_createStatic(TLKSYS_TASKID_SYSTEM, &sMyTimer, 1 * 1000 * 1000, 1, 
                             myTimerCB, NULL); // 1秒周期性定时器
    tlksys_timer_start(TLKSYS_TASKID_SYSTEM, &sMyTimer);
}

小结:

tlksys_timer 模块通过以下方式扩展了 tlkapi 层的定时器功能:

  • 任务隔离:每个任务拥有独立的定时器列表,避免了定时器冲突,使得各自的定时任务遵循原有的优先级运行
  • 线程安全:通过互斥锁保证多线程环境下的安全访问
  • 事件驱动:定时器操作后自动触发任务事件,确保及时处理
  • 接口简化:提供更简洁的接口,隐藏了底层细节

tlkos_timer 系统定时器

tlkos_timer 是 SDK 操作系统抽象层(OSAL)中的定时器管理模块,针对不同的运行环境(裸机环境和 RTOS 环境)提供了两套不同的实现方案。该模块通过统一的接口为上层应用提供定时器功能,屏蔽了底层实现差异。

API 速览:

接口函数 裸机实现 RTOS 实现
tlkos_timer_create 基于 tlkapi_timer_create 基于 RTOS 接口,如 FreeRTOS xTimerCreate
tlkos_timer_destroy 基于 tlkapi_timer_destroy 基于 RTOS 接口,如 FreeRTOS xTimerDelete
tlkos_timer_start 基于 tlkapi_timer_start 基于 RTOS 接口,如 FreeRTOS xTimerStart
tlkos_timer_stop 基于 tlkapi_timer_stop 基于 RTOS 接口,如 FreeRTOS xTimerChangePeriod
tlkos_timer_setPeriod 基于 tlkapi_timer_setPeriod 基于 RTOS 接口,如 FreeRTOS xTimerStart
tlkos_timer_startFromISR 暂不支持 基于 RTOS 接口,如 FreeRTOS xTimerStartFromISR

实现原理:

在裸机环境下,tlkos_timer 模块基于硬件定时器 tlkapi 层的软件定时器列表实现:

  • 使用 MCU 的 Timer1 硬件定时器作为系统节拍源
  • 利用 tlkapi 层的 TlkApiTimerList_t 作为软件定时器管理器
  • 通过硬件定时器中断触发软件定时器处理

在 RTOS 环境下,tlkos_timer 直接封装了 RTOS 的定时器 API:

裸机基于 Timer1 外设中断运行,RTOS(以 FreeRTOS 为例)基于守护线程(极高优先级的线程)运行,两者本质上都是通过高优先级以确保抢占和定时任务的及时性。

使用示例:

// 定时器回调函数
static void keyscan_timer_callback(TlkOsTimerHandle_t timerHandle, void *pUsrArg)
{
    (void) timerHandle;
    (void) pUsrArg;
    key_scan();
}

// 创建并启动定时器
void example_key_scan_timer_init(void)
{
    TlkOsTimerHandle_t timerHandle;
    tlkos_timer_create("key_sacn_timer", 10, 1, keyscan_timer_callback, NULL, &timerHandle);
    tlkos_timer_start(timerHandle);
}

硬件定时器

SDK 定时器硬件资源使用情况

硬件资源 SDK 用处 使用情况 ([x]占用,[ ]空闲)
stimer 无线协议栈 controller 调度器时钟源 单核芯片 [x],双核芯片应用核(D25F)[ ]
timer0 给 audio 任务产生高精度定时周期 audio 功能启用 [x],audio 功能未启用 [ ]
timer1 裸机 tlkos 定时器触发源 RTOS 未启用 [x],RTOS 启用 [ ]
mtimer RTOS 心跳源 RTOS 未启用 [ ],RTOS 启用 [x]

用户可以基于上表自行使用硬件定时器进行高精度定时。

调试方式

Telink SDK 提供了多种调试方式,主要包括 printf API、分级日志系统以及其他调试机制。

使用 RISC-V TDB 软件连接 USB,或者使用 telinkdualmodeaudiotool 软件连接串口,显示 print API 所打印的信息。

以下是 print API 详细介绍:

(1) 基础 Printf APIs

tlkapi_printf:主要的格式化输出函数

#define tlkapi_printf(en, fmt, ...)     
    if (en) {                           
        tlk_printf(fmt, ##__VA_ARGS__); 
    }
  • 参数

    • en:启用标志,非 0 时输出日志
    • fmt:格式化字符串,与标准 C printf相同
    • ...:可变参数列表
  • 使用场景:通用日志输出,可通过第一个参数方便地控制是否启用

(2) 分级日志输出 API

SDK 提供了 5 个级别的日志输出函数,每个函数都带有日志级别标识:

#define tlkapi_warn(flags, pSign, format, args...)  // 警告级别,带<WARN>标识
#define tlkapi_info(flags, pSign, format, args...)  // 信息级别,带<INFO>标识
#define tlkapi_trace(flags, pSign, format, args...) // 跟踪级别,带<TRACE>标识
#define tlkapi_fatal(flags, pSign, format, args...) // 致命错误,带<FATAL>标识
#define tlkapi_error(flags, pSign, format, args...) // 错误级别,带<ERROR>标识
  • 参数

    • flags:调试标志,控制是否输出
    • pSign:签名标识,用于区分不同模块
    • formatargs:格式化字符串和参数
  • 使用场景:按严重程度分类的日志,方便过滤和定位问题

(3) 数组数据打印 API

#define tlkapi_array(flags, pSign, format, pData, dataLen) 
    tlkdbg_array(flags, pSign, format, (uint8_t *)pData, dataLen)
  • 参数
    • pData:指向数据数组的指针
    • dataLen:数据长度
  • 使用场景:打印二进制数据、缓冲区内容等

(4) 高效数据传输 API

#define tlkapi_send_string_data(en, str, pData, len)  // 发送字符串和数据混合模式
#define tlkapi_send_string_u32s(en, str, ...)         // 发送字符串和多个uint32_t数据
#define tlkapi_send_string_u8s(en, str, ...)          // 发送字符串和多个uint8_t数据
#define tlkapi_sendStr(en, pStr)                      // 只发送字符串
#define tlkapi_sendData(en, pStr, pData, dataLen)     // 发送字符串和数据
  • 特点:这些 API 将日志发送到 FIFO 缓冲区,不立即输出,需要debug_handler处理后才会输出
  • 使用场景:高效日志记录,适合高频调用场景

(5) DSP 相关调试 API

对于 DSP 部分,SDK 提供了专用的调试函数:

#define dsp_log_printf(...)  // DSP通用日志输出

// DSP分级日志输出
#define dsp_printf_warn(module, ...)   // 警告级别
#define dsp_printf_debug(module, ...)  // 调试级别
#define dsp_printf_error(module, ...)  // 错误级别
#define dsp_printf_info(module, ...)   // 信息级别

// DSP字符串输出
#define dsp_print_str_warn(module, str)
#define dsp_print_str_debug(module, str)
#define dsp_print_str_error(module, str)
#define dsp_print_str_info(module, str)
  • 参数

    • module:模块标志,用于控制输出
    • 格式化参数与标准 printf 类似

调试级别和调试标志控制

SDK 定义了 5 个调试级别和多种调试标志,用于精细控制输出的日志类型:

typedef enum
{
    TLKAPI_DEBUG_LEVEL_LEVEL1 = TLKAPI_DBG_ASSERT_FLAG | TLKAPI_DBG_FATAL_FLAG | TLKAPI_DBG_ERROR_FLAG |
                                TLKAPI_DBG_WARN_FLAG | TLKAPI_DBG_INFO_FLAG | TLKAPI_DBG_TRACE_FLAG |
                                TLKAPI_DBG_ARRAY_FLAG,  // 输出所有日志
    TLKAPI_DEBUG_LEVEL_LEVEL2 = TLKAPI_DBG_ASSERT_FLAG | TLKAPI_DBG_FATAL_FLAG | TLKAPI_DBG_ERROR_FLAG |
                                TLKAPI_DBG_WARN_FLAG | TLKAPI_DBG_INFO_FLAG,  // 不输出TRACE和ARRAY
    TLKAPI_DEBUG_LEVEL_LEVEL3 = TLKAPI_DBG_ASSERT_FLAG | TLKAPI_DBG_FATAL_FLAG | TLKAPI_DBG_ERROR_FLAG |
                                TLKAPI_DBG_WARN_FLAG,  // 只输出错误和警告
    TLKAPI_DEBUG_LEVEL_LEVEL4 = TLKAPI_DBG_ASSERT_FLAG | TLKAPI_DBG_FATAL_FLAG | TLKAPI_DBG_ERROR_FLAG,
    TLKAPI_DEBUG_LEVEL_LEVEL5 = TLKAPI_DBG_ASSERT_FLAG | TLKAPI_DBG_FATAL_FLAG,
} TLKAPI_DEBUG_LEVEL_ENUM;
  • 使用方法:通过设置相应的宏定义或配置来选择调试级别
#define TLKAPI_DBG_WARN_FLAG   0x02  // 警告日志
#define TLKAPI_DBG_INFO_FLAG   0x04  // 信息日志
#define TLKAPI_DBG_TRACE_FLAG  0x08  // 跟踪日志
#define TLKAPI_DBG_ERROR_FLAG  0x10  // 错误日志
#define TLKAPI_DBG_FATAL_FLAG  0x20  // 致命错误
#define TLKAPI_DBG_ARRAY_FLAG  0x40  // 数组数据
#define TLKAPI_DBG_ASSERT_FLAG 0x80  // 断言信息
#define TLKAPI_DBG_FLAG_ALL    0xFE  // 所有日志

调试输出配置

(1) 调试日志总开关

#define TLK_DEBUG_ENABLE 1  // 1: 启用调试功能,0: 禁用所有调试功能
  • 功能:控制整个 SDK 调试系统的开关
  • 位置:通常在app_config.h文件中定义
  • 作用:当设置为 0 时,所有调试 API(包括 printf、分级日志等)都将被禁用,可有效减少固件大小

(2) USB/UART 调试输出选择

SDK 支持多种调试输出方式,可以根据需要选择合适的方式。具体如下:

USB 调试输出:

#define TLKDBG_CFG_UDB_LOG_ENABLE 1  // 1: 启用USB调试输出
  • 功能:通过 USB 接口输出调试日志
  • 适用场景:需要高速调试输出或没有可用 UART 接口的情况
  • 使用工具:可通过 telinkdualmodeaudiotool 软件连接 USB 查看日志

HCI UART 调试输出:

#define TLKDBG_CFG_HPU_LOG_ENABLE 1  // 1: 启用HCI UART调试输出
  • 功能:通过 HCI UART 接口输出调试日志
  • 适用场景:已有 HCI UART 接口可用的情况

硬件 UART 调试输出:

#define TLKDBG_CFG_HWU_LOG_ENABLE 1  // 1: 启用硬件UART调试输出
  • 功能:通过指定的硬件 UART 接口输出调试日志

配置选项:

// 选择UART端口
#define TLKAPI_DEBUG_UART_PORT DBG_UART_PORT0  // 可选: DBG_UART_PORT0或DBG_UART_PORT1

// 根据端口选择UART通道
#if (TLKAPI_DEBUG_UART_PORT == DBG_UART_PORT0)
    #define DEBUG_UART_CHANNEL UART0
#elif (TLKAPI_DEBUG_UART_PORT == DBG_UART_PORT1)
    #define DEBUG_UART_CHANNEL UART1
#endif

// 配置UART引脚
#define TLKAPI_DEBUG_UART_TX_PIN GPIO_FC_PD3  // 发送引脚
#define TLKAPI_DEBUG_UART_RX_PIN GPIO_FC_PA5  // 接收引脚

// 配置波特率
#define TLKAPI_DEBUG_UART_BAUDRATE 1000000  // 默认1Mbps

其他调试方式

(1) 断言调试

在非 NDEBUG 模式下,SDK 提供了断言功能:

#ifndef NDEBUG
void __assert_func(const char *f, int l, const char *af, const char *e)
{
    tlkapi_printf(1, "assert error: FILE:%s, LINE: %d", f, l);
    TLKSTK_ERROR_DEBUG(1, 0xFF300000);
    while(1); // 死循环,方便调试
}
#endif

(2) GPIO 调试

SDK 支持使用 GPIO 进行简单的调试信号输出:

#define DBG_JUNWEI_CHN12_LOW    gpio_write(GPIO_CHN12, 0)
#define DBG_JUNWEI_CHN12_HIGH   gpio_write(GPIO_CHN12, 1)
#define DBG_JUNWEI_CHN12_TOGGLE gpio_toggle(GPIO_CHN12)
// 类似地定义了其他GPIO通道的调试宏
  • 使用场景:可以用示波器或逻辑分析仪监测 GPIO 状态,用于分析代码执行流程和时间

对 RF 模块进行调试可以使用 rf_set_ble_bb_debugport() 函数,将 RF 模块的状态映射到对应的 GPIO 引脚,方便使用示波器或逻辑分析仪进行分析。

(3) N22 双核调试

对于使用 N22 双核架构的芯片(如 TL751x 等),SDK 提供了专门的双核调试支持:

双核模式使能:

#define MCU_DUAL_CORE_ENABLE 1  // 1: 启用双核模式
  • 功能:控制是否启用双核模式
  • 影响:启用后,系统将同时运行 D25F 核和 N22 核

N22 核日志输出开关:

#define TLK_SM_LOG_ENABLE 1  // 1: 启用N22核日志输出
  • 功能:控制 N22 核的日志输出功能
  • 工作原理:N22 核的日志通过共享内存传输到 D25F 核,再由 D25F 核统一输出

核识别宏:

// 判断当前代码是否在N22核上运行
#if defined(MCU_CORE_N22)
    // N22核特有代码
#else
    // D25F核特有代码
#endif
  • 功能:用于区分不同核上运行的代码
  • 其他相关宏:
    • MCU_CORE_TL751X_N22 - TL751x 芯片的 N22 核

(4) 系统崩溃处理

SDK 提供了系统崩溃时的信息收集和输出功能:

void tlkos_crash(const TlkOsCrashInfo_t * info)
{
    // 收集并输出崩溃信息
    tlkos_crash_printAPI("[OS_CRASH]**********[OS_CRASH]");
    if(info->detailInfo){
        tlkos_crash_printAPI(info->detailInfo);
    }
    // 输出核心信息
    const char * log = tlkos_debug_getCoreInfo();
    while(log)
    {
        tlkos_crash_printAPI(log);
        log = tlkos_debug_getCoreInfo();
    }
    while(1); // 死循环,保持崩溃状态
}

(5) BDT 工具读取内存信息

  • 使用 BDT 工具集成的 Memory Access 工具,可以运行时读取和更改 RAM 和 Flash 的内存内容。

  • 使用 BDT 工具集成的 Tdebug 功能(需要.lst 文件),可以在运行时查看和调试系统状态,包括寄存器值、内存内容等。

使用示例

(1) 基本日志输出

// 输出普通日志
tlkapi_printf(1, "System initialized, version: %s\n", "v1.0.0");

// 输出错误日志
tlkapi_error(DEBUG_FLAG, MODULE_SIGN, "Failed to initialize hardware, error: %d\n", error_code);

// 打印数组数据
uint8_t buffer[16] = {1, 2, 3, 4};
tlkapi_array(DEBUG_FLAG, MODULE_SIGN, "Received data:", buffer, 4);

(2) 控制调试级别

// 在user_config.h或其他配置文件中
#define TLKAPI_DEBUG_LEVEL TLKAPI_DEBUG_LEVEL_LEVEL2  // 设置为级别2

(3) 配置调试输出方式

// 配置UART调试
#define TLK_DEBUG_ENABLE 1
#define TLKDBG_CFG_HWU_LOG_ENABLE 1
#define TLKAPI_DEBUG_UART_PORT DBG_UART_PORT0
#define TLKAPI_DEBUG_UART_TX_PIN GPIO_PD3
#define TLKAPI_DEBUG_UART_RX_PIN GPIO_PA5
#define TLKAPI_DEBUG_UART_BAUDRATE 115200

// 或配置USB调试
#define TLK_DEBUG_ENABLE 1
#define TLKDBG_CFG_UDB_LOG_ENABLE 1

(4) 配置双核调试

// 启用双核调试
#define TLK_DEBUG_ENABLE 1
#define MCU_DUAL_CORE_ENABLE 1
#define TLK_SM_LOG_ENABLE 1  // 启用N22核日志

// N22核上的日志输出示例
#if defined(MCU_CORE_N22)
    tlkapi_info(DEBUG_FLAG, "N22_CORE", "N22 core initialized successfully\n");
#endif

(5) DSP 日志使用

// DSP模块日志输出
#define DSP_AUDIO_MODULE_ENABLE 1  // 在需要的地方启用模块调试

dsp_printf_info(DSP_AUDIO_MODULE_ENABLE, "Audio stream started, sample rate: %d Hz\n", 44100);

内存分配

(1) 功能描述

本 SDK 中提供了动态内存管理接口,包括常用的mallocfreeremalloc等接口。系统提供了两套接口:分别是系统级的内存分配管理接口以及更为底层的通用内存分配接口。

  • 前者接口文件路径为tlkos_api/tlkos_memory.h,该接口同时兼容裸机环境下的自研内存管理实现和 FreeRTOS 原生的内存管理机制。当应用中使能了 FreeRtos,会默认切换到 FreeRtos 原生的内存管理接口。

  • 而后者接口文件路径为tlklib/mem/tlkmem1.h,用户可以使用后者的内存分配及管理接口,来动态定义自身应用中需要使用的内存块,防止因为内存不足或者内存碎片,从而引发系统工作异常等问题。

(2) 规则约束

N/A

(3) 接口说明

内存池数据结构定义:

struct tlkmem1_unit_s
{
    struct tlkmem1_unit_s *prev;        /*指向当前存储单元的前一块地址*/
    struct tlkmem1_unit_s *next;        /*指向当前存储单元的后一块地址*/
    uint32_t               size : 31;   /*定义的存储单元大小*/
    uint32_t               used : 1;    /*是否被使用的标记位*/
};

内存池接口说明:

  • 内存池初始化接口:
int tlkmem1_init(void *pBuffer, uint32_t buffLen);
  • 内存池析构(清除)接口:下列两个接口功能相同,用户可以自行选择调用
void tlkmem1_deinit(void *mem);
void tlkmem1_clean(void *mem);
  • 内存打印接口:执行打印某片内存的实际内容的功能
void tlkmem1_print(void *mem);
  • 内存分配接口:从内存池中分配一块大小为size的内存
void *tlkmem1_malloc(void *mem, uint32_t size);
  • 内存分配接口:从内存池中分配一块大小为size的内存,并且进行清零操作
void *tlkmem1_calloc(void *mem, uint32_t size);
  • 内存分配接口:从内存池中重新分配一块大小为size的内存
void *tlkmem1_realloc(void *mem, void *ptr, uint32_t size);
  • 内存释放接口:释放内存池中的某块内存
int tlkmem1_free(void *mem, void *ptr);

系统中已开辟的内存池:

内存池 大小 (Byte) 功能用途
sTlkOsBareMetalMemBuffer 8 * 1024 裸机或 RTOS 系统中分配的堆栈
sTlkMdiAudMemBuffer 64 * 1024 音频任务使用
sTlkBtMemBuffer 13 * 1024 BT Host 任务使用
... ... ...

注意

  • 以上为系统中默认常开的内存池。其他内存池(包括非常开、调试用途及默认关闭类型)此处不做详细介绍。

内存池的灵活用法介绍:

  • sTlkOsBareMetalMemBuffersTlkBtMemBuffer的内存池的大小不建议更改,已经为优化得比较合理的尺寸,而sTlkMdiAudMemBuffer可以根据实际开发使用的音频算法所需的空间来动态调整,可以通过宏 TLKMDI_AUDMEM_TOTAL_SIZE 来重新定义。

  • 目前音频任务使用的内存池,可以通过宏TLKMW_AUDIO_MEMPOOL_INDEPENDENT开控制开关,一旦关闭后,Audio 任务所需的内存将会尝试从系统堆栈sTlkOsBareMetalMemBuffer中分配,因此以上表的分配情况,sTlkOsBareMetalMemBuffer需要调整至至少 72 KB (64 KB+ 8 KB);看似只是简单的内存池合并、组合,而实际可以为一些非音频场景,需要扩展原本系统堆栈的场景节省内存池空间。举例如下:假设 OTA 功能所需分配系统堆栈为 30 KB,按照原有的分配方式,至少共需要 94 KB(30 KB + 64 KB),而由于 OTA 场景与 Audio 场景并不共存,并且所需内存小于音频任务,因此可以直接使用前面提及的合并后 72 KB 大小的内存池,可以达到节省整体的内存使用的目的。

Flash 地址定义

每种 Flash 容量都有一个基准地址(BASE_ADDR),所有其他区域都通过偏移量相对基准地址计算:

Flash 容量 基准地址 (BASE_ADDR)
1M 0xF2000
2M 0x1EA000
4M 0x3EA000
8M 0x7EA000
16M 0xFEA000

1M Flash 区域分配表(特殊布局)

功能区域 相对基准地址的偏移 地址范围 区域大小 用途描述
SDP ATT 信息 +0x0000 0xF2000 - 0xF3FFF 8KB GATT 服务发现缓存
SMP 配对信息 +0x2000 0xF4000 - 0xF7FFF 16KB BLE 配对密钥信息
安全启动区 +0x6000 0xF8000 - 0xFB000 12KB 安全启动代码和签名
校准数据 +0xC000 0xFE000 - 0xFEFFF 4KB 生产校准参数
MAC 地址存储 +0xD000 0xFF000 - 0xFFFFF 4KB BLE/BT MAC 地址存储

2M+ Flash 通用区域分配表(标准布局)

功能区域 相对基准地址的偏移 地址范围 区域大小 用途描述
SDP ATT 信息 +0x0000 0x*EA000 - 0x*EBFFF 8KB GATT 服务发现缓存
SMP 配对信息 +0x2000 0x*EC000 - 0x*EFFFF 16KB BLE 配对密钥信息
保留区域 +0x6000 0x*F0000 - 0x*F7FFF 32KB 保留/未使用
安全启动区 +0xE000 0x*F8000 - 0x*FB000 12KB 安全启动代码和签名
校准数据 +0x14000 0x*FE000 - 0x*FEFFF 4KB 生产校准参数
MAC 地址存储 +0x15000 0x*FF000 - 0x*FFFFF 4KB BLE/BT MAC 地址存储

用户自定义区域分配表

以下区域使用 TLK_CFG_FLASH_* 宏定义的偏移地址,使用 tinysql 来管理,用户可以在此处添加宏来增加自己的自定义区域,只要注意不与其它已使用区域重复即可,真实地址 = 偏移地址 + flash_full_size - 0x100000:

功能区域 偏移地址 区域大小 用途描述
PBAP 列表 0xC0000 64KB 电话簿访问协议数据
用户设置 0xD0000 8KB 用户配置参数
蓝牙设备信息 0xD2000 8KB 经典蓝牙设备信息
LE 设备信息 0xD4000 8KB BLE 设备信息
音频配置 0xD8000 8KB 音频参数配置

MAC 地址存储区域

  • 大小: 4 KB
  • 用途: 存储 BLE 和 BT MAC 地址
  • 内部细分
    • BASE_ADDR + 0x0000 ~ BASE_ADDR + 0x00FF: BLE MAC 地址 (256 字节)
    • BASE_ADDR + 0x0100 ~ BASE_ADDR + 0x01FF: BT MAC 地址 (256 字节)
    • 剩余空间:保留或用于其他 MAC 相关配置

校准数据存储区域

  • 大小: 4 KB
  • 用途: 生产和测试校准数据

校准数据内部结构

#define CALIB_OFFSET_CAP_INFO        0x00   // 频偏校准信息
#define CALIB_OFFSET_TP_INFO         0x40   // 触摸屏校准信息
#define CALIB_OFFSET_ADC_VREF        0xC0   // ADC 参考电压校准
#define CALIB_OFFSET_FIRMWARE_SIGNKEY 0x180 // 固件签名密钥

多核芯片架构介绍

  • 多核指的是在单块芯片内部集成了多个相互独立的处理器核心,以 TL751x 芯片为例,拥有三个处理器核心,分别为 D25FN22DSP,其中D25FN22RISC-V 架构内核,与架构为 HiFi5 的DSP组成三核异构设计。每个核心都能独立读取指令、执行计算和处理数据,可并行处理不同任务,因此三核架构能显著提升多任务和多线程场景下的运行效率。

  • 本 SDK 在 TL751x 平台上,完成了多个重要工程的适配,因此本章节将以TL751x平台为例进行描述。

功能以及架构

下面框图是目前多核芯片下,不同核心的工作场景以及架构展示:

架构示意图

  • D25F: 这是一款高性能 32 位 RISC-V CPU 内核,负责运行 Host相关任务。D25F 具备 5 级流水线架构、分支预测等技术,还集成了浮点运算单元,能高效处理复杂的系统级任务。在实际应用中,它作为Host核心,可运行 FreeRTOS 等第三方操作系统,同时负责客户定制化应用开发、系统资源调度和外设的复杂管理,并与DSP协同完成音频相关的上层逻辑控制等任务,极大提升了芯片音频应用的灵活性。
  • N22: N22 作为入门级高效 RISC-V 内核,负责运行协议栈。N22以精简架构实现低功耗与高性能的平衡,在处理高数据传输率的协议数据包时表现出色,且能耗低,代码密度高。因此该内核专注于协议栈控制器运行、无线信号收发控制等底层通信任务。
  • DSP: DSP 负责音频信号处理,它支持 768 kHz 的高音频采样率及 24-bit 位深,搭配高性能 Codec,可以保证高保真音质输出。同时支持语音唤醒、麦克风降噪等复杂的音频算法,以适配无线音频领域众多场景的核心需求。

性能介绍

以下是D25FN22 Core 在跑分软件上 CoremarkDhrystone 的性能数据:

说明

  • 由于 DSP 为 HiFi5 架构,此类 MCU 跑分软件数据无法很好体现 DSP 性能,故不作展示。

CoreMark/Dhrystone性能数据

资源介绍

以下是TL751x平台的内存资源分配情况:

Memory资源分配

通信机制

本 SDK 中主要提供两种核间通信机制,邮件通信与共享内存通信机制。

(1) 邮箱通信

  • 邮箱通信方式本质是通过硬件加速的核间 IPC 机制,核心特点是:“少量信息” (目前TL751x单次支持 2 个 word 的 payload 传输)+ “硬件中断” + “低延时”的消息传递;
  • Telink 在TL751x多核芯片上提供了全双工邮箱信息传输通道,多核间互相发起通信请求也可以很好地完成消息传递,不会出现阻塞以及冲突情况;

  • 三核之间的邮箱通信(mailbox)场景如下图:

mailbox示意图

  • D25FN22DSP 两两之间的邮箱信息通道,以通信方向区分,一共对应 6 种 mailbox 中断类型:
typedef enum
{
    FLD_MAILBOX_D25F_TO_DSP_IRQ = BIT(0), 
    FLD_MAILBOX_DSP_TO_D25F_IRQ = BIT(1), 
    FLD_MAILBOX_D25F_TO_N22_IRQ = BIT(2), 
    FLD_MAILBOX_N22_TO_D25F_IRQ = BIT(3),
    FLD_MAILBOX_N22_TO_DSP_IRQ = BIT(4),  
    FLD_MAILBOX_DSP_TO_N22_IRQ = BIT(5), 
} mailbox_irq_status_e;
  • 用法示例:N22D25F 通过 mailbox_n22_set_d25f_msg 接口发送数据,发送长度为 2 个 word 的msg_word后,D25F会触发中断FLD_MAILBOX_N22_TO_D25F_IRQ,此时D25F可以在中断函数中通过mailbox_d25f_get_n22_msg接口读走数据。D25F 在读完最后一个 word 的最后一个 byte 后,硬件会自动清除中断标志位。
/* N22 */
unsigned int msg_word[2] = {0x01, 0x02};
mailbox_n22_set_d25f_msg(&msg_word[0]);

/* D25F */
unsigned int recv_msg_word[2] = {0, 0};
mailbox_d25f_get_n22_msg(&recv_msg_word[0]);

(2) 共享内存

Telink 的 共享内存(Share Memory) 设计支持不定长的数据量传输,内存利用率高,灵活性强。

D25F作为主核,共享内存使用的是D25F的 SRAM 资源,如下代码段,D25F在上电阶段初始化调用下面接口为不同的核间通信通道申请内存。

D25FN22在启动期间,会通过tlkipc_service_coreInfo_sync接口完成核间信息的同步,如共享内存中不同的 FIFO 通道的访问地址等等;D25F会将这一系列的核间信息存储到s_tlkipc_service_coreInfo控制块中,通过mailbox将控制块共享给N22来完成后续的一系列核间同步操作。

int tlk_multi_core_communication_init(void)
{
    /* 邮箱消息模块初始化 */
    tlk_mailbox_service_init();
    /* 共享内存初始化 */
    tlk_share_memory_service_init();
    /* 核间同步初始化 */
    return tlkipc_service_coreInfo_sync();
}

下面列举一个D25FN22核间交互HCI命令的场景来介绍共享内存(share memory)的相关用法以及实现:

/* 1. D25F初始化相关的HCI,并且申请内存 */
share_memory_fifo_init(&sTlkSmFifo[MAIN_CORE_HCI_TX],mainCoreHciTxBuffer, TLK_SM_HCI_TX_BUFFER_SIZE);

/* 2. N22通过`spTlkSmFifo`控制块获得对应的共享内存fifo地址,并且注册对应“接收到共享内存信息”时触发的回调函数 */
share_memory_register_fifo_receive_cb(spTlkSmFifo[CONTROLLER_CORE_HCI_RX],controllerCoreHciRxCb);

此时如果应用中尝试在D25F中下发一条 BT HCI 指令,经过一系列的消息分发,最终 HCI 指令会被作为参数传入到下面的接口中:

/* 3. D25F下发的BT HCI指令最终经过层层分发,会被填入定义在共享内存中的hci fifo中*/
tlk_sm_ret_e retSts = share_memory_data_push(&sTlkSmFifo[MAIN_CORE_HCI_TX],type,data,dataLen);

/* 4. N22如果想要订阅、接收BT HCI类型共享内存消息,则要注册对应的接收回调函数*/
tlk_n22_register_hci_receive_cb(TLK_SHARE_MEMORY_MESSAGE_TYPE_BT, hci_rx_cb);

/* 5. 完成回调函数注册后,N22就可以在自身的共享内存loop处理函数中将接收到的消息进行处理*/
share_memory_data_popAll(spTlkSmFifo[CONTROLLER_CORE_HCI_RX]);

以上例子简单介绍了共享内存在本 SDK 中的具体应用,实际上共享内存还存在一种机制:完成了共享内存消息的发送后,通过组合 mailbox 来通知另外一个核去及时取走消息。这种机制目前只适配于 N22 -> D25F方向的共享内存消息通知,因为N22作为 Controller 运行的核心,时刻保持着对外界设备的通信,需要及时将从外界接收到的消息迅速送往 Host(即D25F)核心进行处理。因此引入了该通知机制,提高共享内存在特定场景的通信效率。

多核启动流程

TL751x的多核架构中,D25F是主控核心,芯片上电启动后D25F核心会最先被启动,以及完成自身一系列的初始化。之后才会通过DMA模块为N22DSP进行代码搬运,以及 Core 的启动等一系列 boot 操作。

(1) N22 启动

由前面的描述可知,N22 启动在主控核心D25F 中进行,启动流程包含以下步骤:

1) N22初始化:调用接口

   void sys_n22_init(unsigned int addr);

接口内完成了如下动作:

  • ZB 模块以及 N22 电源初始化;
  • 初始化 AHB1 总线;
  • 设置 N22 SRAM 启动地址;

2) 配置N22核启动参数为代码搬运准备:

tlkmw_dualcore_boot_cfg_t cfg = {
    .iram_dst_addr = N22_IRAM_ADDR,
    .iram_src_addr = addr,
    .iram_size = n22_ilm_bin_size,

    .dram_dst_addr = N22_DRAM_ADDR | n22_dlm_vma_start,
    .dram_src_addr = n22_dlm_lma_start,
    .dram_size = n22_dlm_bin_size,

    .no_cache_bit = D25F_NO_CACHE_RAM_BIT,
};
tlkmw_dualcore_boot(&cfg);

3) 配置完成后,调用如下接口真正启动N22N22核心开始运行。

void sys_n22_start(void);

(2) DSP 启动

由于 DSP 的功耗会比 D25F 与 N22 都更大,因此在一些非话音场景并不会工作,而是当需要运行相对复杂的音频算法如 NN 降噪算法等等,才会动态地启动。

DSP的启动流程与N22非常类似,启动步骤如下:

1) 调用下列接口完成 DSP 初始化:

    void sys_dsp_init(unsigned int addr);
  • DSP 电源初始化;
  • DSP 时钟初始化以及数字复位;
  • 设置 DSP 启动地址(支持 Flash/RAM boot);

2) 配置启动地址:

tlkmw_dualcore_boot_cfg_t cfg = {
    .iram_dst_addr = 0x2100000,
    .iram_src_addr = addr + iram_bin_begin,
    .iram_size = iram_bin_size,

    .dram_dst_addr = 0x2000000,
    .dram_src_addr = addr + dram_bin_begin,
    .dram_size = dram_bin_size,

    .no_cache_bit = 0x80000000,
};
tlkmw_dualcore_boot(&cfg);

3) 上述配置完成后,调用如下接口真正启动DSPDSP核心开始运行;

void sys_dsp_start(void);

多核芯片固件打包及烧录

本 SDK 中,其他的单核芯片,只需要在0地址完成烧录,而 TL751X平台作为一个多处理器核心芯片需要在不同的 Flash 地址烧录对应 Core 的固件,需要0地址烧录 D25F 固件,在0x100000 烧录 N22 固件,以及在0x200000烧录 DSP 固件;

以 BT/TPSLL TWS 工程为例,完成编译后,BDT 工具烧录设置如下:

bdt_download.png

SDK 中多核芯片对应工程中都也提供了merge_bin.sh脚本文件,用于编译结束后 N22 与 D25F 的 bin 文件打包成一个合并的固件。merge_bin.sh脚本相对智能化,可以自动识别当前CONTROLLER_MODE定义的 controller 模式,自动将对应的HostController固件合并,只需要保证 D25F 和 N22 都完成了编译,编译顺序没有相关限制。

merge_bin脚本选择

下列bttpsll_tws&n22_controller_120.bin是 BT/TPSLL 工程经过merge_bin.sh合并D25FN22 bin 得到的新固件,用户只需要将这个固件在0地址完成烧录,即等于同时完成了 D25F 与 N22 的固件烧录。由于 DSP 固件不在 SDK 中进行编译,且 N22 烧录的结束地址与 DSP 烧录的起始地址之间的间隔较大,如果强行合并会导致烧录效率降低,因此不进行固件合并,仍保留单独烧录的方式。

merge_bin示意

开关机

功能介绍

开关机实际上指的是让 MCU 进入或退出低功耗模式,低功耗模式有三种模式:suspend mode、deepsleep mode 和 deepsleep retention mode。在实际使用过程中,开关机模式使用的是 deepsleep mode。

  • deepsleep mode:此模式下程序停止运行,MCU 绝大部分的硬件模块都断电,仅 PM 硬件模块维持工作。deepsleep mode wake-up 时,MCU 将重新启动,类似于上电的效果,程序会重新开始进行初始化。Deepsleep mode 下,除了 analog register 上有少数几个 register 能保存状态,其他所有 SRAM、digital register、 analog register 全部掉电丢失。

deepsleep 唤醒配置

MCU 的低功耗唤醒源示意图让如下,suspend/deepsleep/deepsleep retention 的唤醒源有多种,在 SDK 中只需要关心 GPIO_PAD 和 timer 唤醒即可。

MCU 低功耗唤醒源示意图

  • 唤醒源 PM_WAKEUP_TIMER 来自硬件 32 kHz timer(32 kHz RC timer or 32 kHz Crystal timer)。32 kHz timer 在 SDK 中已经被正确初始化,user 在使用时不需要任何配置,只需要在 pm_sleep_wakeup() 中设置该唤醒源即可。
  • 唤醒源 PM_WAKEUP_PAD 来自 GPIO 模块,除 MSPI 4 个管脚外所有的 GPIO 的高/低电平都具有唤醒功能。

在开关机业务中,一般使用 GPIO_PAD 作为唤醒源,在配置 GPIO PAD 唤醒 deepsleep mode 时的配置如下:

void pm_set_gpio_wakeup(gpio_pin_e pin, pm_gpio_wakeup_level_e pol, int en)
  • pin: 待配置的 GPIO 号
  • pol: 待配置的 GPIO 电平,WAKEUP_LEVEL_HIGH 表示高电平唤醒,WAKEUP_LEVEL_LOW 表示低电平唤醒
typedef enum
{
    WAKEUP_LEVEL_LOW  = 0,
    WAKEUP_LEVEL_HIGH = 1,
} pm_gpio_wakeup_level_e;
  • en: 待配置的 GPIO 是否使能唤醒功能

deepsleep 的进入与唤醒

设置 MCU 进入睡眠和唤醒的 API 为:

int pm_sleep_wakeup(pm_sleep_mode_e sleep_mode, pm_sleep_wakeup_src_e wakeup_src, pm_wakeup_tick_type_e wakeup_tick_type, unsigned int wakeup_tick);
  • sleep_mode: 待配置的 MCU 进入睡眠模式。
typedef enum
{
    SUSPEND_MODE   = 0x00,
    DEEPSLEEP_MODE = 0xf0, 
    DEEPSLEEP_MODE_RET_SRAM_LOW32K  = 0x01,
    DEEPSLEEP_MODE_RET_SRAM_LOW64K  = 0x03,
    DEEPSLEEP_MODE_RET_SRAM_LOW128K = 0x07, 
    DEEPSLEEP_MODE_RET_SRAM_LOW256K = 0x0f,
    DEEPSLEEP_RETENTION_FLAG = 0x0F,
} pm_sleep_mode_e;
  • wakeup_src: 待配置的 MCU 的唤醒源,有两种唤醒源:PM_WAKEUP_TIMER 和 PM_WAKEUP_PAD,如果 wakeup_src 为 0,那么进入低功耗 sleep mode 后,无法被唤醒。
  • wakeup_tick_type: 待配置的 MCU 的唤醒时间类型。
typedef enum
{
    PM_TICK_STIMER = 0, 
    PM_TICK_32K    = 1,
} pm_wakeup_tick_type_e;
  • wakeup_tick: 待配置的 MCU 的唤醒时间,需要设置 wakeup_tick 来决定 timer 在何时将 MCU 唤醒。

通过 g_pm_status_info.wakeup_src 可以获取 PM 唤醒的类型,唤醒类型有以下值:

typedef enum
{
    FLD_WAKEUP_STATUS_PAD        = BIT(0),
    FLD_WAKEUP_STATUS_CORE       = BIT(1),
    FLD_WAKEUP_STATUS_TIMER      = BIT(2),
    FLD_WAKEUP_STATUS_COMPARATOR = BIT(3),
    FLD_WAKEUP_STATUS_ALL = 0xff,
    FLD_WAKEUP_STATUS_INUSE_ALL = 0x0f,
} pm_wakeup_status_e;
  • FLD_WAKEUP_STATUS_TIMER 这个 bit 为 1,说明当前 sleep mode 是被 Timer 唤醒。
  • FLD_WAKEUP_STATUS_PAD 这个 bit 为 1,说明当前 sleep mode 是被 GPIO PAD 唤醒。
  • FLD_WAKEUP_STATUS_TIMER 和 FLD_WAKEUP_STATUS_PAD 同时为 1 时,表示 Timer 和 GPIO PAD 两个唤醒源同时生效了。

PM

PM 模块是 Telink Bluetooth Audio SDK 蓝牙协议栈中的核心组件,负责管理系统的低功耗模式,包括睡眠(suspend)、深度睡眠保持(deepsleep retention)、WFI(等待中断)等功耗控制功能,以实现蓝牙设备的低功耗运行。

工作原理

Telink IC 有三种低功耗模式:

(1) suspend mode:此时程序停止运行,类似暂停功能。MCU 大部分硬件模块断电,PM 模块维持正常工作,suspend mode 下所有的 IRAM 和 analog register 都能保存状态。

(2) deepsleep mode:此时程序停止运行,MCU 绝大部分的硬件模块都断电,PM 硬件模块维持工作。deepsleep mode wake_up 时,MCU 将重新启动,类似于上电的效果,程序会重新开始进行初始化。Deepsleep mode 下,除了 analog register 上有少数几个 register 能保存状态,其他所有 IRAM、digital register、 analog register 全部掉电丢失。

(3) deepsleep retention mode:相比较于 deepsleep mode,电流稍微偏大,但是无法存储全部的 IRAM 信息。在 Deepsleep retention mode 时,MCU 绝大部分的硬件模块都断电,PM 硬件模块维持工作。 功耗是在 deepsleep mode 基础上增加 retention IRAM 消耗的电流。deepsleep mode wake_up 时,MCU 将重新启动,程序会重新开始进行初始化

SDK 支持 suspend mode 和 deepsleep retention mode。

用户可以根据需要单独调用 deepsleep 功能。

PM 模块特性

PM 模块提供灵活的电源管理模式配置,包括:

  • 多低功耗模式:涵盖睡眠(Suspend)、深度睡眠保持(DeepSleep Retention)、等待中断(WFI)等
  • 灵活配置能力:提供睡眠模式、唤醒源、时间参数等运行时 / 编译时配置
  • 多核协同支持:适配多核架构,实现多核同步和睡眠机制
  • 回调机制:支持睡眠前后回调,满足用户的扩展需求
  • 功耗优化:通过任务调度时序管理、唤醒提前量控制等降低功耗

PM 模块核心机制

(1) 睡眠决策逻辑

  • 预检查:查询 PM 模块使能状态,底层时序是否 busy,睡眠是否允许
  • 模块状态检查:检查 BT/LE/TPSLL 以及用户任务是否 busy
  • 睡眠时间计算:综合用户预期睡眠时间和协议栈底层时序,取最佳时间睡眠时间

(2) 唤醒源管理

  • 支持定时器唤醒,GPIO 唤醒以及组合方式

(3) 硬件设置保存和恢复

  • 睡眠模式如 suspend/deepsleep retention 等模式下,硬件模块可能掉电,PM 模块支持在睡眠前硬件设置保存,睡眠后硬件设置恢复,不需要重新进行硬件配置,提高系统运行效率。

(4) 支持多核架构

  • 通过核间通信(share memory 或者 mailbox 等),共享睡眠信息
  • 主核综合各核的睡眠信息,控制系统进入低功耗状态。

(5) 支持多操作模式

  • 支持裸机和 RTOS 双模式

功能实现

(1) PM 模块初始化

PM 模块初始化包括:

  • 睡眠回调事件注册(也可以在系统运行过程中进行)
  • 设置唤醒源
  • 注册睡眠进入和离开回调处理
  • 使能 PM 模块

相关 API:

  • tlksdk_pm_init:PM 模块初始化,主要功能是初始化 PM 模块内部相关参数
/**
 * @brief   for user to initialize low power mode
 * @param   none
 * @return  none
 */
void tlksdk_pm_init(void);
  • tlksdk_pm_enableSleep:PM 模块使能,传入 1 使能 PM 模块,传入 0 关闭 PM 模块
/**
 * @brief   for user to enable low power mode
 * @param   enable - TRUE: enable low power mode ; FALSE: disable low power mode
 * @return  none
 */
void tlksdk_pm_enableSleep(bool enable);
  • tlksdk_pm_registerPmEventCallback:注册睡眠回调
/**
 * @brief   Register PM Event callBack
 * @param[in]   e - event number, must use element of "pm_ev_flag_t"
 * @param[in]   p - callBack function
 * @return  none
 */
void tlksdk_pm_registerPmEventCallback(u8 e, pm_event_callback_t p);
  • tlksdk_pm_setWakeupSource:设置唤醒源,唤醒源支持 timer 和 GPIO 的组合
/**
 * @brief   for user to set low power mode wake up source
 * @param   wakeup_src - low power mode wake_up source
 * @return  none
 */
void tlksdk_pm_setWakeupSource(pm_sleep_wakeup_src_e wakeup_src);
  • tlksdk_pm_getPMWakeupSRC:获取当前唤醒源
/**
 * @brief  Function to get the wakeup source of mcu.
 * @param  none
 * @return refer to pm_suspend_wakeup_status_e[for TL721X/TL751X] or pm_wakeup_status_e [for B91\B92].
 */
u32 tlksdk_pm_getPMWakeupSRC(void);

说明

  • B91 是指 TLSR921x 系列芯片。
  • tlksdk_pm_is_enabled:判断当前 PM 模块是否在使能状态
/**
 * @brief   Check if the system PM mode is used.
 * @param   none
 * @return  true if system PM mode is used, false otherwise.
 */
bool tlksdk_pm_is_enabled (void);

(2) PM 模块运行

以 suspend 模式为例,PM 模块工作流程:

step1 检查当前是否满足睡眠条件,PM 模块是否使能等。

step2 睡眠前检查,检查 BT/BLE/TPSLL 或者用户当前任务是否 busy,如果 busy 则不进入低功耗

step3 睡眠时间计算,综合协议栈底层时序和用户预期睡眠时间,计算出最佳睡眠时间

step4 硬件设置保存

step5 进入低功耗状态->离开低功耗状态

step6 硬件设置恢复

相关 API:

  • tlksdk_pm_enterSleep:进入低功耗状态,用户可以在需要的地方调用并传入预期睡眠时间,PM 模块会根据当前系统状态决定是否进入低功耗
/**
 * @brief   Check the system's readiness and put the CPU into sleep for entering Power Management (PM) mode.
 * @param   sleep_mode : refer to type: pm_sleep_mode_e in driver/pm.h
 * @param   nxt_task_wakeup_tick: Expected wake - up time.
 * @return 16 - bit unsigned int; 0 indicates successful entry into PM mode, non - zero means an issue prevented entry.
 */
u32 tlksdk_pm_enterSleep(u32 sleep_mode, u32 nxt_task_wakeup_tick);

上位机工具通讯

功能介绍

当前上位机支持 Windows 和 Linux 64 位系统平台,包含串口 OTA、双模 Audio Source 下 BT 和 BLE 扫描配连接等功能。

上位机界面主要有两个部分,串口选择窗口、功能窗口。

uart_tool

(1) 控制代码交互

BT 命令处理函数位于 tlkapp_host_bt_msg.c 的 tlkapp_btmgr_msgHandle。

uart_bt_handle

BLE 命令处理函数位于 tlkapp_lemgrMsg.c 的 tlkapp_lemgr_msgHandle。

uart_ble_handle

根据 msgID 判断是哪个命令,然后调用相应的处理函数。

(2) BT 控制命令

  • 获取 BT 名称:获取开发板的 BT 名称。

  • 设置 BT 名称:设置开发板的 BT 名称。

  • 获取 BT 地址:获取开发板的 BT 地址。

  • 设置 BT 地址:设置开发板的 BT 地址。

  • 查询 BT 设备:进行 inquiry 发现,查询到的设备会显示在搜索列表中。

  • 取消 BT 设备:关闭 inquiry 发现

  • 开启 BT 配对:进行 page 流程。

  • 关闭 BT 配对:取消 page 流程。

  • 连接 BT 设备:连接搜索列表中选的设备,连接成功的设备会显示在连接列表中。

(3) BLE 控制命令

  • 打开 BLE 扫描:打开 BLE 扫描功能,扫描到 BLE 设备会显示在搜索列表中。

  • 关闭 BLE 扫描:关闭 BLE 扫描功能。

  • 连接 BLE 设备:连接搜索列表中选的设备,连接成功的设备会显示在连接列表中。

  • 断开 BLE 连接:断开连接列表中选的设备。

  • 关闭自动连接:关闭自动连接功能。

(4) UART OTA

当前工具支持串口 OTA,通过 select 选择 bin 文件,然后点击开始 OTA 即可。

uart_ota

上位机 log 查看(仅 Windows 平台支持)

(1)通过程序状态栏可以打开 log 日志

uart_dev_tool

(2)选择查看的日志等级

uart_log_level

日志等级分成四种,分别为:

  • 信息:显示开发板输出的 log 日志
  • 详细:显示程序与开发板交互日志
  • 警告:显示程序遇到的警告
  • 错误:显示程序遇到的错误

用户可以选择自己需要查看的日志等级,并通过过滤功能搜索需要的 log 信息。

协议介绍

串口协议为自定义协议格式,分别包含头标识、帧属性、消息包、尾标识等。消息类型主要有 SYSTEM、BT、BLE、Audio 等,每种类型有对应的命令、事件。当前上位机支持部分 BT/BLE 命令和事件响应,详细数据格式参考 BT/BLE 双模串口通信协议文档。

用户区保存

TinySQL 是一个轻量级的简易版嵌入式数据库系统,它提供了一种结构化的方式来存储和管理设备上的持久化数据,例如用户设置、配对设备信息、蓝牙地址等。其的核心思想是将不同类型的数据组织成不同的"磁盘"(disk),每个磁盘负责特定类型的数据存储。这些磁盘利用底层的 tlkapi_save 模块实现可靠的 Flash 存储,用户可不关心底层的硬件 save 实现,直接使用 get/set 接口

TinySQL 的主要组件包括:

  • 核心模块 (tlkmdi_tinySql.c): 管理所有磁盘模块,提供统一的初始化、保存和恢复接口
  • 磁盘模块: 每个磁盘模块负责特定类型的数据存储
    • 用户设置磁盘 (tlkmdi_tinySql_disk_userSetting.c)
    • BT 配对设备磁盘 (tlkmdi_tinySql_disk_pairingDevice.c)
    • 蓝牙 MAC 地址磁盘 (tlkmdi_tinySql_disk_btMac.c)
    • PBAP 磁盘 (tlkmdi_tinySql_disk_pbap.c)
    • LE 磁盘 (tlkmdi_tinySql_disk_le.c)
    • 音频磁盘 (tlkmdi_tinySql_disk_audio.c)

磁盘索引号如代码所示:

typedef enum
{
    tinySql_notFind = 0XFFFF,     // 值表示未找到项目
    tinySql_full    = 0XFFFF,     // 值表示存储已满
    tinySql_nullptr = 0XFFFF,     // 值表示空指针

    tinySql_disk0SaveIndex = 0,   // 磁盘 0 索引
    tinySql_disk1SaveIndex,       // 磁盘 1 索引
    tinySql_disk2SaveIndex,       // 磁盘 2 索引
    tinySql_disk3SaveIndex,       // 磁盘 3 索引
    tinySql_disk4SaveIndex,       // 磁盘 4 索引
    tinySql_disk5SaveIndex,       // 磁盘 5 索引
    tinySql_maxSaveIndex,         // 最大保存索引数

    tinySql_macSaveIndex            = tinySql_disk0SaveIndex,        // MAC 地址磁盘索引
    tinySql_userSettingsSaveIndex   = tinySql_disk1SaveIndex,        // 用户设置磁盘索引
    tinySql_pairingDevicesSaveIndex = tinySql_disk2SaveIndex,        // 配对设备磁盘索引
    tinySql_pbapSaveIndex           = tinySql_disk3SaveIndex,        // PBAP 磁盘索引
    tinySql_leSaveIndex             = tinySql_disk4SaveIndex,        // LE 磁盘索引
    tinySql_audioSaveIndex          = tinySql_disk5SaveIndex,        // 音频磁盘索引

} TinySql_private_e;

每个磁盘模块都需要实现以下接口:

typedef struct
{
    void (*init)(void);              // 初始化函数
    void (*save)(void);              // 保存函数
    void (*restoreFactory)(void);    // 恢复出厂设置函数
} tinySqlDisk_t;

tlkapi_save

tlkapi_save 模块是一个用于在 Flash 存储器上保存数据的组件。它提供了基于双扇区备份机制的数据存储方案,能够在提供一定的掉电保护机制,其主要特征如下:

  • 双扇区备份机制: 使用两个 Flash 扇区进行数据备份,提高数据安全性
  • check 校验: 对保存的数据进行校验,确保数据完整性
  • 智能迁移: 当一个扇区空间不足时,自动迁移到另一个扇区
  • 版本管理: 支持数据版本控制,便于固件升级时的数据兼容性处理工作原理

(1) 工作流程

初始化阶段:

  • 检查两个扇区的有效性
  • 根据标识和版本验证数据有效性
  • 找到最新的有效数据位置

保存阶段:

  • 将数据追加写入当前扇区
  • 包含签名、版本和 CRC 校验信息

迁移阶段:

  • 当当前扇区空间不足时,将所有有效数据迁移到另一个扇区
  • 然后擦除原扇区并切换当前操作扇区

容错机制:

  • 在异常断电等情况下,通过检查签名、版本和 CRC 来恢复有效数据
  • 提供多次重试机制确保数据写入成功

(2) 接口概述

tlkapi_save3_init():初始化保存控制参数并扫描 Flash 中的有效数据。

int tlkapi_save3_init(tlkapi_save_ctrl_t *pCtrl,
                      uint8_t            sign,
                      uint8_t            version,
                      uint16_t           length,
                      uint32_t           address0,
                      uint32_t           address1);

tlkapi_save3_load():从 Flash 存储中加载数据。

int tlkapi_save3_load(tlkapi_save_ctrl_t *pCtrl, uint8_t *pBuff, uint16_t buffLen);

tlkapi_save3_smartSave():智能保存函数,根据可用空间决定直接保存还是迁移。

int tlkapi_save3_smartSave(tlkapi_save_ctrl_t *pCtrl, uint8_t *pData, uint16_t dataLen);

tlkapi_save3_clean():清理保存扇区,使当前数据失效并重置控制参数。

void tlkapi_save3_clean(tlkapi_save_ctrl_t *pCtrl);

一般情况下用户可直接使用 tlkapi_save3_smartSave(),其对 tlkapi_save3_save() 和 tlkapi_save3_migrate() 已进行整合。

异步存储模式

如下图所示,TinySQL 默认采用异步保存机制,主要是出于以下几个原因考虑:

Timer State Machine

  • 减少频繁的 Flash 写入操作,增加硬件寿命,典型如应用短时间内的高频调节音量场合。
  • Flash 存储器的写入操作相比 RAM 访问要慢得多,读写耗时较长(尤其是扇区擦除时间),且读写时程序往往会进入临界区,频繁长时间读写可能会引起部分现场饿死。
  • 使用 tlkmdi_tinySql_suspendSave 接口可以临时关闭 flash 读写,保护关键代码,防止多核 XIP 取指令与 Flash IO 访问冲突。引起总线异常死机。

注意事项

  • 初始化顺序: 在使用任何磁盘功能之前,必须先调用 tlkmdi_tinySql_init() 进行初始化,本 SDK 已在系统线程初始化阶段调用该接口。
  • 保存时机: 数据更改后不会立即保存到 Flash,需要调用 tlkmdi_tinySql_save() 或者等待系统自动保存,本 SDK 使用系统线程挂起定时器异步/关机保存方式。
  • 线程安全: TinySQL 模块使用互斥锁确保线程安全,在多线程环境中可以直接调用接口函数。
  • 内存占用: 由于是异步保存,占用了一定的 RAM 作为缓冲区,用户可根据需要裁剪保存内容。

各磁盘接口介绍

(1) TinySQL 核心接口

//此函数会初始化所有注册的磁盘模块。
void tlkmdi_tinySql_init(void);
//保存所有待处理的数据到 Flash。
int tlkmdi_tinySql_save(void)
//将所有磁盘模块恢复到出厂设置。
void tlkmdi_tinySql_restoreFactorySettings(void);
//检查是否有待处理的保存请求。
//true: 有待处理的保存请求
//false: 没有待处理的保存请求
bool tlkmdi_tinySql_isRequestSave(void);
//启用或禁用保存功能。
//en: 1 表示启用保存功能,0 表示禁用
//当禁用时,保存请求将被忽略。
void tlkmdi_tinySql_setSaveEnable(uint8_t en);
//暂停保存功能。
//此函数增加临界区计数器以防止保存操作。
void tlkmdi_tinySql_suspendSave(void);
//恢复保存功能。
//此函数减少临界区计数器,并可能在需要时触发保存操作。
void tlkmdi_tinySql_suspendSave(void);

(2) 用户设置磁盘

用户设置磁盘负责存储用户的个性化配置,如工作模式、USB 模式、按键配置和蓝牙名称等。

//设置/获取当前工作模式
uint8_t tlkmdi_tinySql_getWorkMode(void);
void tlkmdi_tinySql_setWorkMode(uint8_t mode);
//设置/获取usb模式
uint8_t tlkmdi_tinySql_getUsbMode(void);
void tlkmdi_tinySql_setUsbMode(uint8_t mode);
//设置/获取usb ID
uint16_t tlkmdi_tinySql_getUsbID(void);
void tlkmdi_tinySql_setUsbID(uint16_t usbID);
//设置/获取BT名称
int tlkmdi_tinySql_getBtName(uint8_t *recBuffer);
int tlkmdi_tinySql_setBtName(uint8_t *inBuffer, uint32_t datalen);
//设置/获取按键配置
void tlkmdi_tinySql_getKeyCofnig(keyConfigs_t **key_config_info);
void tlkmdi_tinySql_updateKeyCofnig(keyConfigs_t *key_config_info);

(3) BT 配对设备磁盘

配对设备磁盘用于存储已配对的蓝牙设备信息,包括设备地址、设备类别、链接密钥和设备名称等。

//获取已配对的设备个数
uint32_t tlkmdi_tinySql_getPairingDevicesCount(void);

//清空配对列表
void tlkmdi_tinySql_cleanPairingDevices(void);

//增删改查配对设备列表
int tlkmdi_tinySql_updatePairingDevice(uint8_t *pDevAddr, uint32_t *devClass, uint8_t *pLinkKey, uint8_t *pDevName);
int tlkmdi_tinySql_deletePairingDevice(uint8_t *pDevAddr);
int tlkmdi_tinySql_getPairingDeviceByAddr(uint8_t *pDevAddr, uint32_t *devClass, uint8_t *pLinkKey, uint8_t *pDevName);
int tlkmdi_tinySql_getPairingDeviceByIndex(uint32_t index, uint8_t *pDevAddr, uint32_t *devClass, uint8_t *pLinkKey, uint8_t *pDevName);
int tlkmdi_tinySql_getLastPairingDevice(uint8_t *pDevAddr, uint32_t *devClass, uint8_t *pLinkKey, uint8_t *pDevName);
int tlkmdi_tinySql_searchLastPairingDeviceWithMagicWord(uint32_t magicWord, uint8_t *pDevAddr, uint32_t *devClass, uint8_t *pLinkKey, uint8_t *pDevName);
int tlkmdi_tinySql_setPairingDeviceUserMagicWord(uint8_t *pDevAddr, uint32_t magicWord);

//设置/获取一个配对设备RFC channel ID信息
int tlkmdi_tinySql_getPairingDeviceRfcChid(uint8_t *pDevAddr, void *val, uint8_t type);
int tlkmdi_tinySql_setPairingDeviceRfcChid(uint8_t *pDevAddr, uint16_t val, uint8_t type);

//设置/获取一个配对设备的音量
int tlkmdi_tinySql_getPairingDeviceVolume(uint8_t *pDevAddr, uint8_t isMusic, uint8_t *val, uint8_t *isIos);
int tlkmdi_tinySql_setPairingDeviceVolume(uint8_t *pDevAddr, uint8_t isMusic, uint8_t val, uint8_t isIos);

(4) 蓝牙 MAC 地址磁盘

用于存储蓝牙设备的 MAC 地址。

//设置/获取经典蓝牙、低功耗蓝牙、泰凌2.4g私有协议的设备MAC地址
int tlkmdi_tinySql_getBtMacAddress(uint8_t *recBuffer);
int tlkmdi_tinySql_SetBtMacAddress(uint8_t *inBuffer);
int tlkmdi_tinySql_getLeMacAddress(uint8_t *recBuffer);
int tlkmdi_tinySql_setLeMacAddress(uint8_t *inBuffer);
int tlkmdi_tinySql_getTpsAddr(uint8_t *recBuffer);
int tlkmdi_tinySql_getTpdAddr(uint8_t *recBuffer);

(5) PBAP 磁盘

PBAP (Phone Book Access Profile) 磁盘用于存储电话簿相关信息,主要接口如下,用户可参考注释和 tlkmdi_btpbap.c 使用。

bool tlkmdi_tinySql_getPhoneBookState(void);
int tlkmdi_tinySql_newPhoneBook(uint8_t *btMac);
int tlkmdi_tinySql_addPbapItemBlock(bool isLastOne,uint16_t itemsNum, void *data, uint16_t dataLen);
int tlkmdi_tinySql_getPhoneBookMac(uint8_t * recbuffer);
const uint8_t* tlkmdi_tinySql_getPhoneBookMacPointer(void);
uint16_t tlkmdi_tinySql_getPhoneBookItemNum(void);
const void *tlkmdi_tinySql_searchPhoneBook(tlkMdiTinySqlSearchFunc searchFunc,uint16_t oneItemLen);

RTOS 介绍

SDK 的操作系统抽象层 (OSAL) 是一个为应用程序和协议栈提供统一接口的中间层,允许开发者在裸机和实时操作系统两种模式下切换,目前部分工程和芯片平台暂不支持由于资源限制暂未适配 RTOS。

如图所示,OSAL 采用了适配器模式,为每种支持的操作系统环境提供了对应的实现:

OSAL

  • BareMetal 实现: 位于 tlkos_adapt_layer/baremetal/ 目录下,简单编写了适用于裸机的接口

  • FreeRTOS 实现: 位于 tlkos_adapt_layer/freertos-V5/ 目录下,是对 FreeRTOS API 的封装

通过编译时配置宏 TLK_CFG_RTOS_ENABLE 来决定使用哪种实现。

文件结构如下所示:

tlklib/os/
├── tlkos.h                    // 主头文件
├── tlkos_config.h             // 配置文件
├── tlkos_api/                 // API定义头文件
   ├── tlkos_define.h         // 基础定义
   ├── tlkos_kernel.h         // 内核相关接口
   ├── tlkos_task.h           // 任务管理接口
   ├── tlkos_timer.h          // 定时器接口
   ├── tlkos_semphr.h         // 信号量接口
   ├── tlkos_mutex.h          // 互斥量接口
   ├── tlkos_msgq.h           // 消息队列接口
   ├── tlkos_event.h          // 事件接口
   ├── tlkos_memory.h         // 内存管理接口
   └── tlkos_debug.h          // 调试接口
└── tlkos_adapt_layer/         // 具体实现
    ├── baremetal/             // 裸机实现
    └── freertos-V5/           // FreeRTOS实现

CFG 配置介绍

//基于app_config的TLK_CFG_RTOS_ENABLE配置是否启用RTOS
#if !TLK_CFG_RTOS_ENABLE
#define TLKOS_CFG_BAREMETAL_ENABLE    1
#define TLKOS_CFG_FREERTOS_ENABLE     0
#else
#define TLKOS_CFG_BAREMETAL_ENABLE    0
#define TLKOS_CFG_FREERTOS_ENABLE     1
#endif 

//是否启用低RAM资源消耗配置
#ifndef TLKOS_CFG_USE_LOWER_RAM_SIZE
#if MCU_CORE_TYPE == MCU_CORE_TL752X
#define TLKOS_CFG_USE_LOWER_RAM_SIZE  1
#else
#define TLKOS_CFG_USE_LOWER_RAM_SIZE  0
#endif
#endif

//裸机内存池大小配置
#ifndef TLKOS_CFG_BAREMETAL_HEAP_SIZE
#define TLKOS_CFG_BAREMETAL_HEAP_SIZE (8 * 1024)
#endif

//OS堆的大小配置
#ifndef TLKOS_CFG_OS_HEAP_SIZE
#define TLKOS_CFG_OS_HEAP_SIZE        (30 * 1024)
#endif

//OS心跳硬件的频率
#ifndef TLKOS_CFG_HEART_TIMER_TICK_HZ
#define TLKOS_CFG_HEART_TIMER_TICK_HZ (32000UL) //32768(ext_clock) 32000(internal_clock)
#endif 

//OS心跳的频率
#ifndef TLKOS_CFG_OS_TICK_HZ
#define TLKOS_CFG_OS_TICK_HZ          (1000)     //1ms
#endif

//PLIC中断栈大小配置(WORD)
#ifndef TLKOS_CFG_PLIC_STACK_SIZE_WORD
#define TLKOS_CFG_PLIC_STACK_SIZE_WORD    (1024 * 1)
#endif

//OS Debug配置
#ifndef TLKOS_CFG_DEBUG_ENABLE
#define TLKOS_CFG_DEBUG_ENABLE             (1 && TLK_CFG_RTOS_ENABLE)
#endif

//OS Debug信息打印
#ifndef TLKOS_CFG_DEBUG_INFO_OUT
#define TLKOS_CFG_DEBUG_INFO_OUT           ((!TLKOS_CFG_USE_LOWER_RAM_SIZE) && TLKOS_CFG_DEBUG_ENABLE)
#endif

//OS DebugIO配置
#define TLKOS_CFG_DEBUG_IO_ENABLE          (0 && TLKOS_CFG_DEBUG_ENABLE)
//OS 栈溢出检测配置
#define TLKOS_CFG_DEBUG_STACK_OVERFLOW     (0 && TLKOS_CFG_DEBUG_ENABLE)
//OS malloc失败检测配置
#define TLKOS_CFG_DEBUG_MALLOC_FAIL        (1 && TLKOS_CFG_DEBUG_ENABLE)
//OS CPU占用率检测配置
#define TLKOS_CFG_DEBUG_CPU_USAGE          (0 && TLKOS_CFG_DEBUG_ENABLE)

//OS TICKLESS配置
#define TLKOS_CFG_TICKLESS_ENABLE     (TLK_CFG_SUSPEND_ENABLE)

#define TLKOS_CFG_CHECK_OS_ENABLE_NUM ((TLKOS_CFG_BAREMETAL_ENABLE) + (TLKOS_CFG_FREERTOS_ENABLE))

#if (TLKOS_CFG_CHECK_OS_ENABLE_NUM) != 1
    #error "TLK_OS_CFG_CHECK_OS_ENABLE_NUM NOT EQUAL TO 1"
#endif

//数据段、程序段的分配配置
#define _attribute_os_core_code_ram_sec_ __attribute__((section(".ram_code"))) __attribute__((optimize("O2"))) 
#define _attribute_os_core_code_flash_sec_ __attribute__((optimize("O2"))) 

#if MCU_DUAL_CORE_ENABLE
#define _attribute_os_heap_sec_ __attribute__((section(".iram_data"))) 
#else
#define _attribute_os_heap_sec_ 
#endif

//断言配置
#define TLKOS_ASSERT(x) 

API 介绍

(1) tlkos_kernel

//获取当前是否在irq
int tlkos_get_irqState(void);
//获取当前的内核状态
int tlkos_get_kernelState(void);
//进出临界区接口
void tlkos_enter_critical(void);
void tlkos_leave_critical(void);
//os初始化接口
void tlkos_init(void);
//os启动接口(调度器启动)
void tlkos_start(TlkOsInitFunc_t initFunc);

(2) tlkos_memory

//申请/释放内存接口
void *tlkos_malloc(uint32_t size);
void *tlkos_calloc(uint32_t size);
void tlkos_free(void *ptr);

(3) tlkos_mutex

//创建锁接口
int tlkos_mutex_create(TlkOsMutexHandle_t *mutexHandle);
//销毁锁接口
int tlkos_mutex_destroy(TlkOsMutexHandle_t mutexHandle);
//加锁及解锁接口
int tlkos_mutex_lock(TlkOsMutexHandle_t mutexHandle);
int tlkos_mutex_unlock(TlkOsMutexHandle_t mutexHandle);
//递归锁创建、加锁、解锁接口
int tlkos_recursiveMutex_create(TlkOsMutexHandle_t *recursiveMutexHandle);
int tlkos_recursiveMutex_lock(TlkOsMutexHandle_t recursiveMutexHandle);
int tlkos_recursiveMutex_unlock(TlkOsMutexHandle_t recursiveMutexHandle);

(4) tlkos_semphr

//二值信号量创建接口
int tlkos_semphr_createBinary(TlkOsSemphrHandle_t *semphrHandle);
//计数信号量创建接口
int tlkos_semphr_createCounting(TlkOsSemphrHandle_t *semphrHandle, uint32_t maxCnt, uint32_t initCnt);
//信号量销毁接口
int tlkos_semphr_destroy(TlkOsSemphrHandle_t semphrHandle);
//信号量获取接口
int tlkos_semphr_take(TlkOsSemphrHandle_t semphrHandle, uint32_t blockTimeMs);
//信号量释放接口
int tlkos_semphr_give(TlkOsSemphrHandle_t semphrHandle);
//信号量中断中释放接口
int tlkos_semphr_giveFromISR(TlkOsSemphrHandle_t semphrHandle);

(5) tlkos_task

//任务创建接口,支持动态/静态创建
int tlkos_task_create(TlkOsTaskEnterCB enterCB, const char *pName, uint32_t stackSize, uint32_t priority, void *CBUsrArg, TlkosTaskExtCfg_t *extArg, TlkOsTaskHandle_t *taskHandle);
//任务销毁接口
void tlkos_task_destroy(TlkOsTaskHandle_t taskHandle);
//任务优先级获取/设置接口
uint32_t tlkos_task_getPriority(TlkOsTaskHandle_t taskHandle);
uint32_t tlkos_task_getPriorityFromIsr(TlkOsTaskHandle_t taskHandle);
void tlkos_task_setPriority(TlkOsTaskHandle_t taskHandle, uint32_t priority);
void tlkos_task_setPriorityFromIsr(TlkOsTaskHandle_t taskHandle, uint32_t priority);
//任务阻塞延时接口
int tlkos_task_delayMs(uint32_t delayMs);
//获取当前运行的任务接口
TlkOsTaskHandle_t tlkos_task_getRunningTask(void);
//任务暂停/恢复接口
int tlkos_task_suspend(TlkOsTaskHandle_t taskHandle);
int tlkos_task_resume(TlkOsTaskHandle_t taskHandle);
int tlkos_task_suspendAll(void);
int tlkos_task_resumeAll(void);
//任务栈水位线获取接口
uint32_t tlkos_task_getStackWaterMark(TlkOsTaskHandle_t taskHandle);

(6) tlkos_timer

//定时器创建接口
int tlkos_timer_create(char *pName, uint32_t periodMs, uint32_t autoReload, TlkOsTimerEnterCB CBEnter, void *pUsrArg, TlkOsTimerHandle_t *timerHandle);
//定时器销毁接口
int tlkos_timer_destroy(TlkOsTimerHandle_t timerHandle);
//定时器启动接口
int tlkos_timer_start(TlkOsTimerHandle_t timerHandle);
//定时器重置接口
int tlkos_timer_reset(TlkOsTimerHandle_t timerHandle);
//定时器停止接口
int tlkos_timer_stop(TlkOsTimerHandle_t timerHandle);
//定时器周期设置接口
int tlkos_timer_setPeriodUs(TlkOsTimerHandle_t timerHandle, uint32_t periodUs);
int tlkos_timer_setPeriod(TlkOsTimerHandle_t timerHandle, uint32_t periodMs);

(7) tlkos_event

//事件表(事件标志组)创建接口
int tlkos_event_createTab(uint32_t evtTabLen,TlkOsEventTabHandle_t *evtTabHandle);
//事件表销毁接口
int tlkos_event_destroyTab(TlkOsEventTabHandle_t evtTabHandle);
//订阅事件并注册callback接口
int tlkos_event_regDealCB(TlkOsEventTabHandle_t evtTabHandle,uint32_t index,TlkOsEventDealCB cb);
//触发事件接口
int tlkos_event_set(TlkOsEventTabHandle_t evtTabHandle,uint32_t index);
int tlkos_event_setFromIsr(TlkOsEventTabHandle_t evtTabHandle,uint32_t index);
//阻塞等待事件接口
int tlkos_event_wait(TlkOsEventTabHandle_t evtTabHandle,uint32_t blockTimeMs);
//获取事件接口
int tlkos_event_get(TlkOsEventTabHandle_t evtTabHandle,uint32_t *evt);

(8) tlkos_msgq

//消息队列创建接口
int tlkos_msgq_create(TlkOsMsgQHandle_t *pMsgQHandle, uint32_t msgMaxSize, uint32_t qLength);
//消息队列销毁接口
int tlkos_msgq_destroy(TlkOsMsgQHandle_t msgQHandle);
//消息队列发送接口
int tlkos_msgq_send(TlkOsMsgQHandle_t msgQHandle, uint8_t *pData, uint32_t dataLen, uint32_t blockTimeMs);
//阻塞等待消息接口
int tlkos_msgq_wait(TlkOsMsgQHandle_t msgQHandle, uint8_t *pBuff, uint32_t *recLen, uint32_t buffLen, uint32_t blockTimeMs);

SDK 中已有线程

OSAL

SDK 中主要包含以下几个线程:

  • SYSTEM 任务线程:这是系统的基础任务线程,负责系统级功能管理,包括:

    • 系统电源管理
    • Flash 存储管理
    • USB 接口管理
    • 按键和 LED 设备管理
    • 调试接口管理
    • 日志系统
  • AUDIO 任务线程:音频任务线程专门处理音频相关的功能:

    • 音频调度管理
    • UAC 音频类设备控制
    • 音频播放和录制控制
    • DSP 相关处理

注意

  • audio 任务会创建子线程用以解码。
  • HOST 任务线程:HOST 任务线程负责蓝牙协议栈相关的主机功能:
    • HCI 层处理
    • 经典蓝牙协议支持
    • BLE 协议支持
    • TPSLL 协议支持
    • 优先级为 TLKSYS_TASK_HOST_PRIORITY(4)

RF 测试相关(BQB/EMI)

进入 BQB 或 EMI 测试需要使用 BDT 工具中的 BQB、EMI tool,下载相应的固件,然后连接仪器进行测试。

BDT BQB 上位机设置

点击 Tool-> BQB tool 打开工具,在 Test Select 处选择 BT,在下方 BR Pow 和 EDR Pow 处设置功率值,点击 Download,将固件下载到开发板中。

打开BDT BQB上位机

下载完成后连接仪器进行测试即可。

BDT EMI 上位机设置

点击 Tool -> EMI tool 打开工具,在 Test Select 处选择 BT,点击 Download,将固件下载到开发板中。

打开BDT EMI上位机

打开 EMI test 界面,可以设置 carrier 单 tone 发送模式,也可以设置 carrierdata 带数据的 continue 模式(可选择是否跳频跟发送数据类型),参数的设置可以设置 channel、power(勾上 slice 可以直接设置 slice 值),包格式:

选择BT

打开 Non-Signaling Test 界面,可以设置 TX burst 模式跟 RX 接收,TX burst 模式可以设置无限发包或发 1000 个包,可选择发送数据类型,RX 接收可点击 RX Count 查看收包数量,点击 RSSI 查看接收包的 RSSI 值。参数的设置可以设置 channel、power(勾上 slice 可以直接设置 slice 值),包格式:

选择Non-Signaling Test

BOOT 相关及 OTA

当前 SDK 已完成公司通用 Bootloader 与 OTA 方案的适配,全面兼容单核及多核硬件平台,方案的具体实现细节可参考《通用 BOOT 和 OTA 实现方案》文档。

OTA 固件文件格式

SDK 提供专用的脚本自动化的生成。后续考虑编写对应的上位机生成,方便客户对可选 feature 的配置。APP 的文件格式主要包含 Total FW DescriptorsFW entityFW entity 又包含了 Cur Fw DescriptorCur FW Data 两部分。具体文件格式参考下图:

ota_file_param

  • Total FW Descriptors:大小为 4 KB,是对所有固件的汇总描述,用户可通过解析此区域得到当前区域的固件个数、当前固件的总大小、以及支持的文件组合等。

  • FW entity:

    • Cur FW Descriptor,描述当前固件实际的启动地址、VID、PID、Version 等信息。
    • Cur FW Data:当前固件实际的有效数据。

在 SDK 中,文件数据的解析格式代码如下:

typedef struct {
    uint32_t img_version;
    uint32_t img_valid_size;
    uint32_t fw_number;
    uint32_t total_size;
    uint32_t fw_group_number;
    struct sTlk_fw_group_list_t *fw_group_list;
    struct sTlk_fw_descriptors_list_t *fw_descpts_list;   //FW Descripotrs List
    uint8_t recv[16];
    sTlk_cur_fw_entity_crc_t img_crc;
}sTlk_total_fw_descriptors_t;
  • Total FW Descriptors数据格式:对其所描述的所有固件的汇总,其中 FW Group List 和 FW Descriptors List 均是以链表的方式存储。表示每个 Total FW Descriptors 所描述的固件数量和文件组数量是可变的,方便拓展。

(1) 固件描述符汇总表

类型 SIZE 说明
Version Word Total FW Descriptors 的版本号,用于支持固件升级流程的版本匹配
Valid Size Word Total FW Descriptors 的实际有效数据大小,用于界定有效信息的边界
FW Count Word 当前描述符所记录的固件(bin)数量
Total Size Word 本次生成的 APP 固件总大小,用于判断是否支持“乒乓升级”(双分区备份升级)的空间条件
FW Group count Word 固件组的数量(至少 1 种),支持多 bin 固件时可划分多个固件组
FW Group List Array 固件组的具体内容,以链表形式存储,链表节点数由FW Group count决定,节点格式参考下表
FW Descriptors List Array 单个固件描述符的链表存储,节点数由FW Count决定,节点格式参考下表(每个节点长度固定)
Resv Array 16 字节预留字段,用于后续功能扩展
CRC Array 32 字节的 Total FW Descriptors 校验值,Boot 启动或 OTA 升级流程中需先校验此值,确保描述符本身的完整性与合法性
  • FW Group List数据格式:Fw Group List实际以链表的形式存储,有几个节点就代表了有几个文件组,用于 APP 的切换。一个文件组包含固件的个数记录在Bin countBin TypeBin Version的组合用来查找固件,组合数量取决于Bin count;为综合考虑单核、多核芯片的多 bin 切换方案,SDK 引入了文件组的概念。多 bin 的切换实际体现为文件组的切换。配合Boot and OTA Configuration Zone中的Fw Setting可以确认要切换哪种文件组。为了兼容单核、多核芯片,实际每个文件组的固件的数量不定。例如单核芯片每个文件组实际固件数为 1,而多核芯片文件组实际固件最少为 1,上限则取决于芯片类型(例如 TL751x 上限为 3)。可通过Bin TypeBin Version,遍历Total FW Descriptors中的FW Descriptors List链表,找到唯一的某个固件。

(2) 固件组列表格式

类型 SIZE 说明
Bin count Word 标识当前固件组内包含的固件(bin)数量,用于明确组内固件的数量边界
Bin Type Word 固件的类型标识,支持自定义解析:标准值(如 01=MCU、02=N22 等),FF 表示无效类型
Bin Version Word 固件的版本号,与Bin Type组合可唯一确定对应固件的存储地址
resv Word 预留字段,用于后续功能扩展
  • FW Descriptors List数据格式:某个固件的信息。注意,此处的Bin Start仅记录对应固件在通过脚本打包后的固件中的偏移地址,并非实际的启动地址。OTA 接收完整的OTA APP FW后,需要解析这里的Bin Start,找到对应的固件后,解析出Cur FW Descriptor中的启动地址,将固件搬运到实际的启动地址运行。

(3) FW Descriptors List

类型 SIZE 说明
Bin Type Word Byte0: 当前 bin 的类型,支持客户做自定义格式的解析 01-MCU 02-N22 03-DSP 04-BOOTLOADER...(大于等于 0F 为客户私有代码,boot 不参与搬运)
Byte1: 启动类型:01-Flash 启动、02-RRAM 启动、03-RAM 启动、04-直接跳转运行、0F-客户自定义等;Boot 需要解析此参数来决定需要将固件搬运到哪里
Byte2-Byte3:resv
Bin Version Word 当前 bin 的版本,与 Bin Type 组合可以找到唯一的 bin
Bin Start Word 当前 bin 的偏移地址
Bin size Word Cur FW Descriptor + Cur FW DATA + Total CRC
resv Array 16 Bytes
  • Cur FW Descriptor数据格式:此处的数据为固件的头信息。

Cur FW Descriptor(当前固件描述符,4 字节对齐):

类型 SIZE 说明
PID Word 全 F 时默认跳过,通常用于标识产品 ID,特殊值下不参与解析
VID Word 全 F 时默认跳过,通常用于标识厂商 ID,特殊值下不参与解析
启动地址 Word 固件实际的启动地址(对应 Flash 存储地址),用于 Bootloader 定位执行入口
Feature Map Array(8 字节) 固件支持的功能集合,包含压缩/解压缩、加解密、数字校验、固件完整性等能力,客户可按需选择性支持(部分功能标注为待完善)
Resv Array(12 字节) 预留字段,用于后续功能扩展
CRC Array(32 字节) 当前固件描述符的 CRC 校验值,用于验证描述符自身的完整性与合法性
  • Cur FW Data数据格式:此处的数据为固件的实际数据。

Cur FW Data(当前固件数据,4 字节对齐):

类型 SIZE 说明
payload Array 固件的实际二进制数据(Bin 文件内容),是固件功能的载体
CRC Array(32 字节) 校验值,覆盖范围为「当前固件描述符(Cur FW Descriptor)+ payload」,用于验证固件数据与描述符的整体完整性

Boot 介绍

在 Boot 阶段,boot_loader 程序首先会检测是否进入 UART DFU 模式,如果进入 UART 的 DFU 模式,可以在该模式下进行固件的 DFU,boot_loader 下的 DFU 是直接擦除 A 区的代码并直接将新的代码下载到 A 区,DFU 成功后直接重启;若不进入 UART 的 DFU 模式,会检测是否进入最小系统模式进行无线 OTA,否则会检测在备份区域 B 是否有新的固件进行加载,如果有新的固件并且校验合法,则会将新的固件搬运到 A 区让然后重启;如果检测到备份区域 B 的不合法的,则直接区检查 A 区的相关参数并跳转到 APP 运行区域(在进行代码启动时,首先会检查固件的Total FW Descripotrs区域的数据是否合法,若此区域数据无效,默认进入 UART 的 DFU 模式等待固件的更新;其次会检查各个固件的合法性)。

在 boot_loader 正常加载 APP 应用程序后,如果是单核,则直接运行 APP 的业务;如果是多核芯片,在主核运行起来后,会主动去搬运副核的固件。

boot_loader 的启动流程如下:

boot_loader

boot_loader 的多核启动流程如下:

boot_loader多核启动流程

在 APP 正常运行时,如果是多核芯片,主核会通过以下两个函数去查找副核代码在法 flash 的存储位置,然后将副核的代码搬运到对应的 RAM 位置。

uint32_t tlkmw_getN22StartUpAddrFromFlash(void)
{
    return tlkmw_getBinStartUpAddrFromFlash(BINX_N22);  
}

uint32_t tlkmw_getDSPStartUpAddrFromFlash(void)
{
    return tlkmw_getBinStartUpAddrFromFlash(BINX_DSP);  
}

OTA 传输流程

OTA 数据传输流程如下:

ota_trans

OTA 使用说明

在使用 OTA 功能时,需要使能该功能,需要打开对应的宏定义(使用 OTA 功能时必须选择 boot_loader 启动)。

#define TLK_MW_USER_CTRL_ENABLE 1

(1) OTA 固件生成 (Linux)

在进行 OTA 时,需要生成用于 OTA 的固件,由于 OTA 固件包含一些特殊的信息,因此需要经过特定的脚本去转化,具体的流程如下:

  • 编译 Application,生成对应的固件,并保存在指定目录下。
  • 执行脚本文件firmware_generation.sh,按照提示选择对应的选项即可。

shell_cmd

(2) OTA 固件生成 (Windows)

脚本基于 Python 3.7 以上版本开发,开发者需要首先完成 Python 环境配置,具体步骤可参考相关文档。

  • OTA GUI 工具:路径为 telink_b91m_bluetooth_src/tlk_bluetooth_src/shell/ota/ota_gui.py,需要支持 Tkinter 模块,Windows 原始支持。

  • OTA 脚本:路径为 telink_b91m_bluetooth_src/tlk_bluetooth_src/shell/otatlk_ota.py

说明

  • B91m 是指 TLSR921x、TLSR922x、TLSR952x 系列芯片。

脚本使用:

根据实际情况修改脚本中的参数,执行脚本即可完成 OTA 升级。

文件不存在时,选择 None 即可。

python3 tlk_ota.py
// or
python tlk_ota.py
d25f_bin = tlk_bin_file_info("ota/bt_interphone.bin", 0x13040, type=0x01)
n22_bin = tlk_bin_file_info("ota/bt_interphone_controller.bin", 0x50020000, type=2)
dsp_bin = tlk_bin_file_info("ota/dsp_audio_sdk_v0.1.0.2_for_ram_boot.bin", 0x200040, type=3)

OTA GUI 使用方法:

通过 Python 的核心工具,可以打包成可执行文件,具体方式咨询 AI。

pyinstaller -F -W ota_gui.py

也可以每次运行脚本工作

python3 ota_gui.py
// or
python ota_gui.py

OTA 的 GUI 界面如下,用户可以根据芯片不同,选择 D25F/N22/DSP 三种固件进行升级。

  • Address 对应的数值不需要用户修改,Version 是固件的版本,用来维护系统版本,默认是 1。

  • Boot 文件可选,如果有 Boot 文件会生成 Boot+OTA 固件,可以直接从 0 地址烧录。

  • OTA 生成的文件默认'tlk_ot_file.bin',可以自行修改,Timestamp 勾选后,生成的 OTA 文件名称会带时间戳,方便用户多版本 Debug 时使用。

  • 点击'Generate OTA'按钮,生成 OTA 文件。

ota_tool

  • 如下图所示,选择的 D25F 和 N22 固件,以及选择了 Boot 文件,默认会生成 OTA 文件和 OTA+Boot 文件。

  • 文件名分别是'tlk_ota_file.bin'和'tlk_ota_file_with_boot.bin'。

ota_tool_select

  • 如果勾选了 Timestamp,生成的文件名会包含时间戳。

  • 文件名分别是'tlk_ota_file_2025-12-05 14-34-55.bin'和'tlk_ota_file_with_boot_2025-12-05 14-34-55.bin'。

ota_tool_select_timestamp

(3) 固件下载

在首次使用 BDT 下载固件时,需要先烧录 boot_loader 程序,再下载 application 固件(该固件为经过特定脚本转化的固件)。

  • 烧录 boot_loader 程序,烧录的位置0x00000000(boot_loader文件在telink_b91m_bluetooth_src/boot_loader);
  • 烧录 application 固件,烧录的位置0x00012000;

注意

  • 如使用脚本合成固件时把 boot_loader 和 application 固件打包在一起,则烧录位置为0x00000000

(4) APP—BLE 使用

安装对应的 APP,将要升级的 ota 固件放在手机中,目录不限制。

  • app 安装完成后,打开 app,界面如下:

ota_app

  • 点击刷新按钮进行设备查询,查询界面如下:

ota_app1

  • 选择设备进行连接,然后进入连接界面。

ota_app2

  • 连接设备,点击CONNECT按钮,进行设备连接,连接成功后CONNECT会变为DISCONNECT

ota_app3

  • 文件导入:点击"Bin file path"选择对应的 OTA 固件。

  • 升级:点击"OTA"按钮进行升级,升级过程中会显示升级进度,升级完成后会显示升级结果。

(5) APP—UART 使用

  • 打开 UART 升级 APP,选择正确的 UART 端口,打开串口:

ota_uart1

  • 点击select bin file选择对应的固件;

  • 点击start OTA按钮进行升级,升级过程中会显示升级进度,升级完成后会显示升级结果。

ota_uart2

OTA 相关代码目录实现

SDK 的 OTA 功能集成于 user_ctrl 模块中,该模块挂载在 TLKSYS_TASKID_SYSTEM 线程下运行, 核心负责处理来自用户 APP 的各类数据与指令,涵盖 OTA 升级、音量调节、音频参数配置、灯效控制等基础功能,同时支持用户扩展自定义 APP 功能。通过 sTlkMwUsrCtrlTaskList 来对不同 task 的数据进行链式的存储,所有的数据存储区均是根据数据长度动态开辟的。同时封了异步调用的接口来保证线程安全, 目前 SDK 最多支持来自 4 个 task 的数据,但需要注意的是,同一时刻允许处理多路的非 OTA 数据,但只允许处理一路 OTA 的数据。user_ctrl 模块默认处于关闭状态,若需启用该模块,需开启宏定义 TLK_MW_USER_CTRL_ENABLE。user_ctrl 模块的系统调用框架如下图:

模块框架

User_ctrl 模块的核心 task 管理数组 sTlkMwUsrCtrlTaskList 的数据结构类型如下:

static sTlkMwUsrCtrlTaskNode_t sTlkMwUsrCtrlTaskList[TLKMW_USER_CTRL_CHN_MAX_NUM]; //TLKMW_USER_CTRL_CHN_MAX_NUM默认为4

typedef struct{
    uint32_t taskID;                            //唯一的taskID,用于区分唯一的链路。例如BT双链接时,传入acl_handle用于区分对应的设备链路
    sTlkMwUsrCtrlBufferNode_t *pBufferHead;     //数据的链表头
}sTlkMwUsrCtrlTaskNode_t;

typedef struct sTlkMwUsrCtrlBufferNode{
    uint8_t type;                               //数据类型,OTA、KEY、LED等
    uint8_t channel;                            //数据通道,UART、BT_SPP、BT_ATT、BLE等, refer to TLKMW_OTA_TRANS_CHN_XXX.
    uint16_t buffer_size;                       //此节点待处理的数据长度
    uint8_t *pBuffer;                           //此节点待处理的数据
    struct sTlkMwUsrCtrlBufferNode *pNext;      //指向此任务的下一个数据指针
}sTlkMwUsrCtrlBufferNode_t;

数据推送接口,此接口可以在不同的线程下调用,接口内部有做线程安全保护。数据推送完毕后会唤醒 tlkmw_user_ctrl_common_handler 来处理数据,tlkmw_user_ctrl_common_handler 中会遍历每个 task 节点,将所有数据处理完毕。这里会根据 sTlkMwUsrCtrlBufferNode_t 中的 type 来判断数据类型,分配不同的处理函数,本章节重点关注 OTA 数据的处理,其他类型的数据处理需要根据具体客户应用来实现。

int tlkmw_userctrl_pushDataToTask(uint32_t taskID, uint8_t *pData, uint16_t dataLen);

以下章节重点介绍 User_Ctrl 模块中 OTA 组件的代码结构及使用方法。

OTA 组件的代码目录结构如下:

目录结构

  • tlk_ota_types.h : OTA 组件内部使用的宏定义
  • tlk_ota_timer_port.* : OTA 模块使用的定时器的抽象接口,均为弱定义,客户可自行实现。
  • tlk_ota_timer_port_example.c : OTA 模块使用的定时器的抽象接口的系统层实现,跟当前 SDK 强绑定。
  • tlk_ota_interface_port.* : OTA 模块使用的接口抽象,均为弱定义,客户可自行实现。
  • tlk_ota_protocol_common.* : 当前 SDK 用于 OTA 协议处理的通用接口。封装了部分通用 OTA 方案和以前的 BLE OTA 方案的通用接口,如用户区保存、收发接口注册等。
  • ble_previous_protocol/* : 兼容以前的 BLE OTA 方案,目前暂未实现。
  • general_protocol/tlk_ota_general_desc_parse.* : 通用 OTA 协议的描述符解析。
  • general_protocol/tlk_ota_general_protocol_port.* : 通用 OTA 接收数据组包的处理,及对应不同 opcode 处理的抽象接口,均为弱定义,客户可自行实现。
  • general_protocol/tlk_ota_general_protocol_port_example.c : 通用 OTA 接收数据处理的具体实现。

(1) 组件初始化

OTA 组件的控制参数结构如下:

typedef struct {
    uint8_t optChn;     //记录当前正在OTA的通道,refer to TLKMW_OTA_TRANS_CHN_XXX
    uint8_t resv[3];    //预留
    uint32_t taskID;    //记录当前正在OTA的taskID,SDK中目前记录为无线/有线设备的传输handle,用来在多链接场景下区分OTA的链路,客户可自行定义,但注意,此taskID必须唯一

    sTlkMwUnitIntf_t intf[TLKMW_OTA_TRANS_CHN_MAX]; //记录每个通道对应的数据收发接口,接收接口目前统一注册到tlk_ota_general_protocol_recv_data,后续解析逻辑一致,发送接口由各个OTA链路自行注册
    sTlkMwNotifyUnit_t notifyCB[TLKMW_OTA_NOTIFY_ARRAY_NUM];    //记录OTA过程中需要通知的回调函数,OTA组件会将OTA process的关键节点和状态依次调用此处记录的回调函数通知给关心的APP
}sTlkMwOtaCommon_t;

OTA 组件初始化函数如下:

int tlkmw_ota_common_init(void)
{
    /*OTA 模块内部使用的参数初始化*/
    OTA_MEMSET(&sTlkMwCommonCtrl, 0, sizeof(sTlkMwOtaCommon_t));
    for (uint8_t i = 0; i < TLKMW_OTA_NOTIFY_ARRAY_NUM; i++) {
        sTlkMwCommonCtrl.notifyCB[i].threadID = 0xFFFF;
    }
    /*OTA 模块内部使用的NVDS接口初始化*/
    tlk_nvds_ota_interface_init(&sTlk_ota_interface);
    /*SDK本地的用户区起始地址的保存,用于动态计算备份区域的地址*/
    if (sTlk_ota_interface.nvds_ota_user_load != NULL && sTlk_ota_interface.nvds_ota_user_load((uint8_t*)&sTlk_boot_ota_cfg, sizeof(sTlk_boot_and_ota_cfg_t), NULL) == OTA_NONE) {
        unsigned int address = tlk_nvds_get_full_size() + TLK_CFG_FLASH_PBAP_LIST_ADDR - 0x100000;//The corresponding offset for the last 1M.
        sTlk_boot_ota_cfg.user_area_addr = address;

        tlk_nvds_ota_userarea_addr_save((uint8_t*)&sTlk_boot_ota_cfg, sizeof(sTlk_boot_and_ota_cfg_t), NULL);
    }
    /*通用OTA方案的协议初始化*/
    if (tlk_ota_general_protocol_init(&sTlk_ota_interface) != OTA_NONE) {
        return -OTA_INITERR;
    }
    /*兼容以前的BLE OTA方案,暂未实现*/
    if (tlk_ota_ble_previous_protocol_init(&sTlk_ota_interface) != OTA_NONE) {
        return -OTA_INITERR;
    } 

    return OTA_NONE;
}

int tlk_ota_general_protocol_init(nvds_ota_Interface_t *pInterface)
{
    /*通用OTA协议控制参数的初始化*/
    OTA_MEMSET(&ota_general_ptotocol_ctrl, 0, sizeof(tlk_ota_general_protocol_t));

    if (pInterface == NULL || pInterface->nvds_ota_malloc == NULL) {
        return -OTA_INITERR;
    }
    /*通用 OTA 协议接收数据组包处理中接收缓存的初始化*/
    ota_general_ptotocol_ctrl.p_recv_cache_buff = (uint8_t*)pInterface->nvds_ota_malloc(TLKMW_OTA_TRANS_MAX_PACK_SIZE);
    if (ota_general_ptotocol_ctrl.p_recv_cache_buff == NULL) {
        return -OTA_INITERR;
    }

    /*通用OTA协议内部使用的参数初始化,同时这里会加载当前运行区的固件image信息到sTlkMwCurImgHeader中*/
    if (tlk_ota_general_protocol_detail_init(pInterface) != OTA_NONE) {
        return -OTA_INITERR;
    }

    /*通用OTA协议接收数据接口注册*/
    tlkmw_ota_register_chn_recv_interface(TLKMW_OTA_TRANS_CHN_UART, tlk_ota_general_protocol_recv_data);
    tlkmw_ota_register_chn_recv_interface(TLKMW_OTA_TRANS_CHN_BT_SPP, tlk_ota_general_protocol_recv_data);
    tlkmw_ota_register_chn_recv_interface(TLKMW_OTA_TRANS_CHN_BT_ATT, tlk_ota_general_protocol_recv_data);
    tlkmw_ota_register_chn_recv_interface(TLKMW_OTA_TRANS_CHN_BLE_GENERAL_MODE, tlk_ota_general_protocol_recv_data);

    return OTA_NONE;
}

(2) 数据接收处理

OTA 系统层数据传输:

SDK 中的 OTA 模块是一个相对独立的组件,用户层需要将对应的数据传输给 OTA 模块,由模块内部来进行对数据的解析及处理。目前 SDK 的 OTA 支持多通道管理,用户可自行实现 OTA 数据收发接口,并注册到对应通道,即可实现自定义的 OTA 方案。通道定义如下,客户可自行添加自定义通道:

enum {
    TLKMW_OTA_TRANS_CHN_NONE = 0,
    TLKMW_OTA_TRANS_CHN_UART,   
    TLKMW_OTA_TRANS_CHN_BT_SPP,
    TLKMW_OTA_TRANS_CHN_BT_ATT,
    TLKMW_OTA_TRANS_CHN_BLE_GENERAL_MODE,
    TLKMW_OTA_TRANS_CHN_BLE_PREVIOUS_MODE,
    TLKMW_OTA_TRANS_CHN_MAX,
};

SDK 目前已经实现了 TLKMW_OTA_TRANS_CHN_UART、TLKMW_OTA_TRANS_CHN_BT_SPP、TLKMW_OTA_TRANS_CHN_BT_ATT、TLKMW_OTA_TRANS_CHN_BLE_GENERAL_MODE 四个通道的 OTA。默认情况下,这四个通道的数据接收接口统一在 OTA 组件中按照如下方式管理:

    tlkmw_ota_register_chn_recv_interface(TLKMW_OTA_TRANS_CHN_UART, tlk_ota_general_protocol_recv_data);
    tlkmw_ota_register_chn_recv_interface(TLKMW_OTA_TRANS_CHN_BT_SPP, tlk_ota_general_protocol_recv_data);
    tlkmw_ota_register_chn_recv_interface(TLKMW_OTA_TRANS_CHN_BT_ATT, tlk_ota_general_protocol_recv_data);
    tlkmw_ota_register_chn_recv_interface(TLKMW_OTA_TRANS_CHN_BLE_GENERAL_MODE, tlk_ota_general_protocol_recv_data);


int tlk_ota_general_protocol_recv_data(uint32_t taskID, uint8_t *pData, uint16_t dataLen, void *UserArg)
{
    (void)taskID;
    if (pData == NULL || dataLen < 4) {
        return -OTA_PARAMERR;
    }

    uint16_t offset = 0;
    uint8_t channel = pData[0];
    offset += 1;

    /*channel 过滤,不允许多路OTA数据包进入OTA处理*/
    if (ota_general_ptotocol_ctrl.channel != 0 && ota_general_ptotocol_ctrl.channel != channel) {
        return -OTA_CHANNELERR;
    }

    ota_general_ptotocol_ctrl.channel = channel;

    offset += 2; //dataLen

    uint8_t opcode = pData[3];
    uint16_t info = 0;

    if (opcode > TLK_OTA_OPC_MAX) {
        return -OTA_PARAMERR;
    }
    offset += 1;

    OTA_ARRAY_TO_UINT16L(pData, offset, info);
    offset += 2;

    uint8_t pack_flag = info & 0x03;
    uint16_t pack_index = (info & 0xFFFC) >> 2;

    if (pack_flag > TLK_OTA_PACK_TYPE_END) {
        OTA_PRINTF("[OTA] pack flag error:%d", pack_flag);
        return -OTA_PARAMERR;
    }

    if (pack_flag == TLK_OTA_PACK_TYPE_COMPLETE) {
        /*无需拼包,直接到下一级处理,解析opcode*/
        tlk_ota_general_protocol_deal(ota_general_ptotocol_ctrl.channel, opcode, pData+offset, dataLen-offset, UserArg);
    } else {
        /*需要拼包*/
        if ((dataLen + ota_general_ptotocol_ctrl.recv_cache_len) > TLKMW_OTA_TRANS_MAX_PACK_SIZE) {
            OTA_PRINTF("[OTA] cache buff null");
            return -OTA_PARAMERR;
        }

        if (ota_general_ptotocol_ctrl.recv_cache_opcode != TLK_OTA_OPC_NONE && opcode != ota_general_ptotocol_ctrl.recv_cache_opcode) {
            tlk_ota_general_protocol_clear_recv_cache();
            OTA_PRINTF("[OTA] opcode error:%d, cache:%d", opcode, ota_general_ptotocol_ctrl.recv_cache_opcode);
            return -OTA_PARAMERR;
        }

        if (pack_index != ota_general_ptotocol_ctrl.recv_cache_index) {
            tlk_ota_general_protocol_clear_recv_cache();
            OTA_PRINTF("[OTA] pack index error:%d, cache:%d", pack_index, ota_general_ptotocol_ctrl.recv_cache_index);
            return -OTA_PARAMERR;
        }

        if (pack_flag == TLK_OTA_PACK_TYPE_END) {
            /*拼包完成,进入下一层处理, 解析opcode*/
            OTA_MEMCPY(ota_general_ptotocol_ctrl.p_recv_cache_buff + ota_general_ptotocol_ctrl.recv_cache_len, pData+offset, dataLen-offset);
            ota_general_ptotocol_ctrl.recv_cache_len += (dataLen-offset);
            tlk_ota_general_protocol_deal(opcode, opcode, pData+offset, dataLen-offset, UserArg);
            tlk_ota_general_protocol_clear_recv_cache();
        } else {
            ota_general_ptotocol_ctrl.recv_cache_opcode = opcode;
            ota_general_ptotocol_ctrl.recv_cache_index += 1;;
            OTA_MEMCPY(ota_general_ptotocol_ctrl.p_recv_cache_buff + ota_general_ptotocol_ctrl.recv_cache_len, pData+offset, dataLen-offset);

            ota_general_ptotocol_ctrl.recv_cache_len += (dataLen-offset);
        }
    }
    return OTA_NONE;
}

(3) 数据发送处理

数据发送接口的注册在各个模块中自行注册,若未注册对应通道的发送接口,则不支持此通道的 OTA 传输,具体实现参考如下:

    tlkmw_ota_register_chn_send_interface(TLKMW_OTA_TRANS_CHN_UART, tlkmdi_comm_sendOTADat);
    tlkmw_ota_register_chn_send_interface(TLKMW_OTA_TRANS_CHN_BT_SPP, tlkmdi_btspp_otaSendData);
    tlkmw_ota_register_chn_send_interface(TLKMW_OTA_TRANS_CHN_BT_ATT, tlkmdi_btatt_otaSendData);
    tlkmw_ota_register_chn_send_interface(TLKMW_OTA_TRANS_CHN_BLE_GENERAL_MODE, blc_svc_tlkOtaV2_sendData);

(4) 协议解析处理

下面是协议内部处理 OTA 流程的管理参数的数据结构:

    typedef struct {
        uint8_t curFwNum;           //当前正在传输的固件
        uint8_t timeout;            //OTA超时时间
        uint8_t status;             //当前的OTA状态
        uint8_t channel;            //当前的OTA传输通道
        uint16_t shakeIntv;         //下位机与上位机的握手间隔
        uint16_t cache_size;        //待写入flash的缓存数据大小,存够 TLKMW_OTA_WRITE_CACHE_SIZE 后写入flash

        uint32_t backAddr;          //固件备份区域的地址
        uint32_t saveOffset;        //flash 保存的偏移量,实际应用时需+backAddr

        uint32_t fwDataStartOffset; //本地记录的第curFwNum个固件相对于整个OTA固件来说的开始偏移量
        uint32_t fwDataTotalSize;   //本地记录的第curFwNum个固件的总大小
        uint32_t fwDataRecvSize;    //本地记录的第curFwNum个固件已经接收到的大小,配合fwDataTotalSize确认此固件的接收完成
        uint32_t fwDataRecvNumb;    //本地记录的第curFwNum个固件已接收到的数据编号,用来做丢包检测
        uint32_t fwDataPendNumb;    //检测丢包后,记录待接收的序号
        uint32_t flash_save_size;   //记录缓存数据已经写入flash的大小,用于确认整个OTA固件的接收完成

        tlk_ota_timer_handle_t timer;   //OTA 定时任务, 用于记录OTA是否超时

        uint8_t *p_cache_buffer;    //写入falsh的缓存数据buffer, 目前是在检测到OTA开始后,自动malloc了TLKMW_OTA_WRITE_CACHE_SIZE大小
        nvds_ota_Interface_t *ota_intf; //OTA 的NVDS 接口
    }sTlkMwOta_t;

OTA 协议解析处理的接口如下,具体实现详见代码中实现。此接口内会在检测到开始 OTA 时,创建定时器任务来检测 OTA 超时。

    void tlk_ota_general_protocol_deal(uint8_t channel,uint8_t opcode, uint8_t *pData, uint16_t dataLen, void *UserArg);

固件的 Flash 存储采用「边擦边写」机制实现:协议内置 TLKMW_OTA_WRITE_CACHE_SIZE 字节(目前默认是 4 KB)的接收缓存,接收到的固件数据会先写入该缓存;当缓存数据量达到 TLKMW_OTA_WRITE_CACHE_SIZE 阈值时,将缓存数据写入 Flash 并清空缓存。若新增接收数据与缓存历史数据的总量超过待写入阈值,则优先将满足阈值的部分写入 Flash,剩余数据暂存于缓存中。具体实现接口如下:

 static void tlkmw_ota_common_data_cache_deal(uint8_t *pData, uint16_t dataLen, bool isLast)
{
    if (sTlkMwOtaCtrl.p_cache_buffer == NULL || sTlkMwOtaCtrl.ota_intf->nvds_ota_write == NULL) {
        OTA_PRINTF("tlkmw_ota_common_data_cache_deal: cache buffer is NULL");
        return;
    }

    if (sTlkMwOtaCtrl.cache_size + dataLen >= TLKMW_OTA_WRITE_CACHE_SIZE) {
        uint16_t spaceLeft = TLKMW_OTA_WRITE_CACHE_SIZE - sTlkMwOtaCtrl.cache_size;
        uint16_t writeSize = (dataLen > spaceLeft) ? spaceLeft : dataLen;

        OTA_MEMCPY(sTlkMwOtaCtrl.p_cache_buffer + sTlkMwOtaCtrl.cache_size, pData, writeSize);
        sTlkMwOtaCtrl.cache_size += writeSize;

        sTlkMwOtaCtrl.ota_intf->nvds_ota_eraseSector(sTlkMwOtaCtrl.backAddr + sTlkMwOtaCtrl.saveOffset);
        sTlkMwOtaCtrl.ota_intf->nvds_ota_write(sTlkMwOtaCtrl.backAddr + sTlkMwOtaCtrl.saveOffset, sTlkMwOtaCtrl.cache_size, sTlkMwOtaCtrl.p_cache_buffer);
        sTlkMwOtaCtrl.saveOffset += sTlkMwOtaCtrl.cache_size;
        sTlkMwOtaCtrl.flash_save_size += sTlkMwOtaCtrl.cache_size;
        sTlkMwOtaCtrl.cache_size = 0;

        if (dataLen > writeSize) {
            OTA_MEMCPY(sTlkMwOtaCtrl.p_cache_buffer + sTlkMwOtaCtrl.cache_size, pData + writeSize, dataLen - writeSize);
            sTlkMwOtaCtrl.cache_size += (dataLen - writeSize);
        }
    } else {
        OTA_MEMCPY(sTlkMwOtaCtrl.p_cache_buffer + sTlkMwOtaCtrl.cache_size, pData, dataLen);
        sTlkMwOtaCtrl.cache_size += dataLen;
    }

    if (isLast) {
        if (sTlkMwOtaCtrl.cache_size > 0) {
            sTlkMwOtaCtrl.ota_intf->nvds_ota_eraseSector(sTlkMwOtaCtrl.backAddr + sTlkMwOtaCtrl.saveOffset);
            sTlkMwOtaCtrl.ota_intf->nvds_ota_write(sTlkMwOtaCtrl.backAddr + sTlkMwOtaCtrl.saveOffset, sTlkMwOtaCtrl.cache_size, sTlkMwOtaCtrl.p_cache_buffer);
            sTlkMwOtaCtrl.saveOffset += sTlkMwOtaCtrl.cache_size;
            sTlkMwOtaCtrl.flash_save_size += sTlkMwOtaCtrl.cache_size;
            sTlkMwOtaCtrl.cache_size = 0;
        }
    }
}

音频通路及算法

音频调度器原理与实现

整体架构

音频调度器位于 tlkapp/audio/ 目录下,主要由以下几个核心模块组成:

文件 功能
tlkapp_audioScheduler.c 调度器核心实现
tlkapp_audioScheduler.h 调度器接口定义
tlkapp_audioModinf.c 音频模块接口管理
tlkapp_audioMsg.c 音频消息处理
tlkapp_audioCtrl.c 音频播放控制
tlkapp_audio.c 音频任务主入口

任务状态机

调度器采用状态机管理音频任务,状态定义如下:

typedef enum {
    TLKAPP_AUDIO_SCHEDULER_TASK_STATE_NOINIT = 0,   // noinit
    TLKAPP_AUDIO_SCHEDULER_TASK_STATE_IDLE,         // idle
    TLKAPP_AUDIO_SCHEDULER_TASK_STATE_PAUSED,       // paused
    TLKAPP_AUDIO_SCHEDULER_TASK_STATE_READY,        // ready
    TLKAPP_AUDIO_SCHEDULER_TASK_STATE_RUNNING,      // running(internal)
    TLKAPP_AUDIO_SCHEDULER_TASK_STATE_MUTEX,        // mutex(internal)
    TLKAPP_AUDIO_SCHEDULER_TASK_STATE_CRASH,        // crash(internal)
} TLKAPP_AUDIO_SCHEDULER_TASK_STATE_ENUM;

状态转换说明:

当前状态 操作 目标状态 触发条件
NOINIT 创建/更新任务 IDLE/PAUSED/READY 调用 updateTask()
IDLE 启动 READY 调用 updateTask()state=READY
READY 调度选中 RUNNING 调度器选择该任务
RUNNING 暂停 PAUSED/READY 被高优先级任务抢占 或 调用 pauseTask()
RUNNING 关闭 NOINIT 调用 deleteTask()
PAUSED 恢复 READY 调用 resumeTask()
任意 异常 CRASH 任务执行出错

优先级系统

调度器支持 8 级优先级(0-7),优先级越高越先被调度(高优先级抢占低优先级)、同优先级轮转调度。

默认优先级表:

音频类型 优先级 说明
TLKAUD_TYPE_TONE 7 提示音最高优先级
TLKAUD_TYPE_CC_BT_VOICE 5 BT 电话
TLKAUD_TYPE_BT_VOICE_FORWARD 6 BT 音频转发
TLKAUD_TYPE_SIDETONE 4 通透模式
TLKAUD_TYPE_ANC 6 ANC 降噪
TLKAUD_TYPE_CC_BT_MUSIC 2 BT 音乐
TLKAUD_TYPE_A2DP_OUT 2 A2DP(source) 输出
TLKAUD_TYPE_LEA_UC_MUSIC 2 LE Audio (client) 音乐
TLKAUD_TYPE_LEA_US_MUSIC 2 LE Audio (server) 音乐
TLKAUD_TYPE_INTRTPHONE 3 internal phone 专用场景
TLKAUD_TYPE_TPH_AUDIO 1 TPSLL 音频
TLKAUD_TYPE_UAC_AUD 1 UAC 音频
TLKAUD_TYPE_UAC_LOCAL_AUDIO 1 UAC 本地音频

任务节点结构

每个音频任务对应一个 tlkapp_audioScheduler_node_t 节点:

struct tlkapp_audioScheduler_node_s {
    uint32_t                          taskId;           // taskID = handle + (optype << 16)
    struct tlkapp_audioScheduler_node_s *prev;          // prev node
    struct tlkapp_audioScheduler_node_s *next;          // next node
    struct tlkapp_audioScheduler_node_s *sameDevTask;  // same device task(used for quick switch)
    tlkapp_audioScheduler_taskInfo_t    info;           // task info
    tlkapp_audioScheduler_extraInfo_t   extraInfo;      // extra info(callback etc.)
};

taskInfo 结构:

typedef struct {
    uint8_t optype;     // audio operation type,reference to TLKAUD_TYPE_ENUM
    uint8_t audioType;  // audio type(MUSIC/VOICE),reference to TLKAPP_AUDIO_SCHEDULER_AUDIO_TYPE_ENUM
    uint8_t priority;   // priority,reference to TLKAPP_AUDIO_SCHEDULER_PRIORITY_ENUM
    uint8_t state;       // current state,reference to TLKAPP_AUDIO_SCHEDULER_TASK_STATE_ENUM
} tlkapp_audioScheduler_taskInfo_t;

调度器核心数据结构

typedef struct {
    uint32_t                       busyTimer;           // busy check timer
    uint32_t                       cfg;                 // config flag
    tlkapp_audioScheduler_checkBusyCB checkBusyCB;      // busy check callback
    tlkapp_audioScheduler_node_t  *busyTask;           // current busy task
    tlkapp_audioScheduler_node_t  *runningTask;        // current running task
    tlkapp_audioScheduler_node_t  *readyTaskList[8];   // ready task list by priority
    tlkapp_audioScheduler_node_t  *idleTaskList[8];     // idle task list by priority
    TlkApiTimer_t                  timer;               // scheduler timer
} tlkapp_audioScheduler_t;

核心调度流程

调度器的核心调度逻辑在 tlkapp_audioScheduler_coreSch() 函数中实现:

调度算法流程:

1. 遍历优先级(从高到低)
   for (i = 7; i >= pThreshold; i--)

2. 在每个优先级的readyTaskList中查找
   while (node != NULL)

3. 检查任务状态是否为READY
   if (node->info.state == TLKAPP_AUDIO_SCHEDULER_TASK_STATE_READY)

4. 执行busy检测
   tlkapp_audioScheduler_switchBusyCheck()
   - 如果设备忙,等待5秒超时

5. 执行状态切换
   tlkapp_audioScheduler_coreSchCB()
   - 暂停当前运行任务
   - 启动新任务

6. 更新任务状态
   tlkapp_audioScheduler_taskToRunning()
   - 当前任务: RUNNING → READY (放入readyTaskList)
   - 新任务: READY → RUNNING

配置标志说明

sameDev(同设备) 是 audio dongle 应用衍生的概念,即做 Central 连接同一个耳机时,BT 通话和 BT 音乐是两种场景,但其实是同一设备,可以进行绑定。产生该概念的原因是为了能够刷新就绪列表,dongle 挂断电话时能自动切到同一设备的耳机的音乐上面。

配置标志 说明
CFG_SAME_DEVICE_REFRESH_L2H 同设备任务优先级从低到高刷新
CFG_SAME_DEVICE_REFRESH_H2L 同设备任务优先级从高到低刷新
CFG_SCH_RESUME_AUTO2READY Resume 任务自动转为 Ready
CFG_SCH_IDLE_AUTO2RESUME Idle 任务自动恢复
CFG_IDLE_PREEMPTIVE_RUNNING Idle 可抢占 Running 任务
CFG_NOT_AUTO_START_TASK 不自动启动任务
CFG_SWITCH_BUSY_CHECK 切换时忙检测

预设配置:

// Headset default setting
TLKAPP_AUDIO_SCHEDULER_CFG_SCH_HEADSET_DEFAULT = CFG_SCH_IDLE_AUTO2RESUME | CFG_SWITCH_BUSY_CHECK

// Dongle default setting
TLKAPP_AUDIO_SCHEDULER_CFG_SCH_DONGLE_DEFAULT = 
        CFG_SAME_DEVICE_REFRESH_H2L | CFG_SAME_DEVICE_REFRESH_L2H |
        CFG_SCH_IDLE_AUTO2RESUME | CFG_SCH_RESUME_AUTO2READY |
        CFG_IDLE_PREEMPTIVE_RUNNING | CFG_NOT_AUTO_START_TASK

// UAC default setting
TLKAPP_AUDIO_SCHEDULER_CFG_SCH_UAC = CFG_SCH_IDLE_AUTO2RESUME | CFG_NOT_AUTO_START_TASK

音乐播放流程(BT Music 为例)

BT Music 播放完整流程

BT music playback process

关键代码路径

(1) 状态回调入口 (tlkmdi_bt_music.c):

static void tlkmdi_bt_music_state_change_cb(uint16_t handle, uint8_t state)
{
    if (state == TLK_STATE_OPENED) {
        if (!s_tlk_mdi_bt_music_env.enable) {
            tlkmdi_audio_sendStartEvt(TLKAUD_TYPE_CC_BT_MUSIC, handle);
        } else if (btp_a2dpsnk_getStatus(handle) == BTP_A2DP_STATUS_STREAM) {
            tlkmdi_audio_sendStartEvt(TLKAUD_TYPE_CC_BT_MUSIC, handle);
        }
    } else if (state == TLK_STATE_PAUSED || state == TLK_STATE_CLOSED) {
        tlkmdi_audio_sendCloseEvt(TLKAUD_TYPE_CC_BT_MUSIC, handle);
    }
}

(2) 事件消息构建 (tlkmdi_audio.c):

int tlkmdi_audio_sendStartEvt(uint8_t audChn, uint16_t handle)
{
    return tlkmdi_audio_sendStartEvtEx(audChn, handle, 0Xff);
}

int tlkmdi_audio_sendStartEvtEx(uint8_t audChn, uint16_t handle, uint8_t priority)
{
    uint8_t buffer[4];
    buffer[0] = audChn;                    // audio channel type
    buffer[1] = priority;                  // priority
    buffer[2] = (handle & 0x00FF);         // handle low byte
    buffer[3] = (handle & 0xFF00) >> 8;    // handle high byte
    return tlksys_sendMsg(TLKSYS_TASKID_AUDIO, TLKSYS_AUD_MSGID_START_EVT, buffer, 4);
}

(3) 任务创建入口 (tlkapp_audioMsg.c):

static int tlkapp_audio_startEvtDeal(uint8_t *pData, uint8_t dataLen)
{
    uint8_t optype = pData[0];
    uint16_t handle = ((uint16_t)pData[3] << 8) | pData[2];

    uint8_t audioType = TLKAPP_AUDIO_SCHEDULER_AUDIO_TYPE_MUSIC;
    if (optype == TLKAUD_TYPE_CC_BT_VOICE || 
        optype == TLKAUD_TYPE_LEA_US_VOICE || 
        optype == TLKAUD_TYPE_BT_VOICE_FORWARD) {
        audioType = TLKAPP_AUDIO_SCHEDULER_AUDIO_TYPE_VOICE;
    }

    uint32_t taskID = handle + ((uint32_t)optype << 16);

    if (optype == TLKAUD_TYPE_CC_BT_VOICE && 
        !tlkmdi_audio_btif_allowedCreateScoWithoutHfp(handle)) {
        return tlkapp_audioScheduler_resumeTask(taskID);
    }

    tlkapp_audioScheduler_taskInfo_t info = {
        .audioType = audioType,
        .optype    = optype,
        .priority  = tlkapp_audioScheduler_getDefaultPriority(optype),
        .state     = TLKAPP_AUDIO_SCHEDULER_TASK_STATE_READY,
    };

    return tlkapp_audioScheduler_updateTask(taskID, info, TLKAPP_AUDIO_SCHEDULER_NO_CHANGED);
}

(4) 模块 Switch 回调 (tlkmdi_bt_music.c):

bool tlkmdi_bt_music_switch(uint16_t handle, uint8_t status)
{
    if (status == TLK_STATE_OPENED) {
        g_bt_music_enable_flag = 1;
        s_tlk_mdi_bt_music_env.acl_handle = handle;
        s_tlk_mdi_bt_music_env.enable     = true;
        tlkmdi_btmusic_switch_in(handle);  // enter music mode, config audio path params
    } else {
        g_bt_music_enable_flag = 0;
        s_tlk_mdi_bt_music_env.enable = false;
        bt_music_close_codec();
        tlkmdi_btmusic_switch_out(handle); // exit music mode, release audio path params
    }
    return true;
}

音乐抢占和恢复流程

抢占场景说明

在蓝牙音频应用中,语音通话(Voice)具有比音乐播放(Music)更高的优先级。当有来电或需要通话时,系统会自动抢占正在播放的音乐,暂停音乐并开始通话。

优先级对比:

  • BT Music: 优先级 2
  • BT Voice: 优先级 5

Voice 抢占 Music、恢复 Music 流程

Music preemption and recovery process

抢占关键代码

核心调度切换 (tlkapp_audioScheduler.c):

static inline bool tlkapp_audioScheduler_coreSchCB(
    tlkapp_audioScheduler_node_t *node, 
    bool isAutoOpenNewTask)
{
    tlkapp_audioScheduler_node_t *nowRunningTask = tlkapp_audioScheduler.runningTask;

    // 1. pause current running task
    if (nowRunningTask != NULL) {
        tlkapp_audio_modinfSwitch(
            nowRunningTask->info.optype, 
            (uint16_t)nowRunningTask->taskId, 
            TLK_STATE_PAUSED);
    }

    if (isAutoOpenNewTask == false) {
        tlkapp_audio_closeHandler();
        return true;
    }

    // 2. start new task
    return tlkapp_audioScheduler_nodeEnterRunnningCB(node);
}

任务状态更新 (tlkapp_audioScheduler.c):

static inline void tlkapp_audioScheduler_taskToRunning(
    tlkapp_audioScheduler_node_t *node, 
    bool isAutoOpenNewTask)
{
    tlkapp_audioScheduler_node_t *nowRunningTask = tlkapp_audioScheduler.runningTask;

    if (isAutoOpenNewTask) {
        // remove from ready list
        tlkapp_audioScheduler_nodeRemoveFromList(node);
        // set as running task
        tlkapp_audioScheduler.runningTask = node;
        tlkapp_audioScheduler_nodeSetNewStateWithChgCB(node, 
            TLKAPP_AUDIO_SCHEDULER_TASK_STATE_RUNNING);
    } else {
        // same device task move to front
        tlkapp_audioScheduler_sameDevToFirst(nowRunningTask);
        tlkapp_audioScheduler.runningTask = NULL;
    }

    // original running task set as ready state
    if (nowRunningTask != NULL) {
        tlkapp_audioScheduler_nodeSetNewStateWithChgCB(nowRunningTask,
            TLKAPP_AUDIO_SCHEDULER_TASK_STATE_READY);

        if (nowRunningTask->info.priority != node->info.priority) {
            // different priority, move to front
            tlkapp_audioScheduler_nodePushFrontToList(
                nowRunningTask, 
                &tlkapp_audioScheduler.readyTaskList[nowRunningTask->info.priority]);
        } else {
            // same priority, round-robin move to back
            tlkapp_audioScheduler_nodePushBackToList(
                nowRunningTask, 
                &tlkapp_audioScheduler.readyTaskList[nowRunningTask->info.priority]);
        }
    }
}

音量调节原理

音量控制概述

音量调节采用分层架构:

(1) 应用层:接收用户按键,调用 tlkapp_audio_volumeCtrl()

(2) 调度层:将音量操作分发给当前音频任务或暂停任务

(3) 模块层:各音频模块实现具体的音量操作(如 AVRCP 协议)

音量控制流程

Volume control process

关键代码

音量控制入口 (tlkapp_audioCtrl.c):

int tlkapp_audio_volumeCtrl(uint8_t isInc)
{
    uint8_t volType;
    if (isInc) {
        volType = TLKAUD_OPCODE_VOLUME_INC;
    } else {
        volType = TLKAUD_OPCODE_VOLUME_DEC;
    }

    // operate current running task first
    const tlkapp_audioScheduler_node_t *nowTask = 
        tlkapp_audioScheduler_getRunningTask();
    if (nowTask != NULL) {
        bool ret = tlkapp_audio_modinfOperate(
            (uint16_t)nowTask->taskId, 
            nowTask->info.optype, 
            &volType, 1);
        return ret ? TLK_ENONE : -TLK_EFAIL;
    }

    // if no running task, find paused music task
    const tlkapp_audioScheduler_node_t *taskNode = 
        tlkapp_audioScheduler_SchPausedTask(
            false, 
            TLKAPP_AUDIO_SCHEDULER_AUDIO_TYPE_MUSIC);
    if (taskNode == NULL) {
        return -TLK_ENOOBJECT;
    }

    uint8_t optype = taskNode->info.optype;
    bool res = tlkapp_audio_modinfOperate(
        (uint16_t)taskNode->taskId, 
        optype, 
        &volType, 1);
    return res ? TLK_ENONE : -TLK_EFAIL;
}

BT Music 音量处理 (tlkmdi_bt_music.c):

bool tlkmdi_bt_music_operate(uint16_t handle, uint8_t opcode, uint8_t *pdata, uint16_t dataLen)
{
    (void)pdata;
    (void)dataLen;

    switch (opcode) {
    case TLKAUD_OPCODE_VOLUME_INC:
    {
        // increase volume
        tlkmdi_audio_btif_VolumeOperate(handle, true, true);
    } break;
    case TLKAUD_OPCODE_VOLUME_DEC:
    {
        // decrease volume
        tlkmdi_audio_btif_VolumeOperate(handle, false, true);
    } break;
    // ... other operation codes
    default:
        return false;
    }
    return true;
}

音量调节注意事项

(1) 运行任务优先:音量调节优先作用于当前正在播放的任务

(2) 暂停任务可调:当没有运行任务时,可调节暂停任务的音量设置

(3) 协议同步:音量变化通过 AVRCP/HFP 协议同步到手机端

(4) 本地存储:音量值通常会保存到 Flash,用于下次开机恢复

如何添加一个自定义的音频场景

步骤总览

添加自定义音频场景需要完成以下新增和修改:

  • 新增 .c 实现文件:实现新的音频场景。
  • 新增 .h 头文件:声明对应的接口。
  • 添加新的音频类型:在 TLKAUD_TYPE_ENUM 枚举中添加新的音频类型。
  • 设置默认优先级:为新的音频场景设置默认优先级。
  • 注册音频模块:在 spTlkAppAudioModinfs 结构体中注册新的音频模块。
  • 包含所需头文件:添加相关头文件引用。

Add Custom Audio Scene Steps

步骤 1: 定义音频类型

tlksys_define.hTLKAUD_TYPE_ENUM 枚举中添加新类型:

typedef enum
{
    TLKAUD_TYPE_TONE = 0,
    TLKAUD_TYPE_CC_BT_VOICE,
    TLKAUD_TYPE_CC_BT_MUSIC,
    // ... current supported types

    // add custom audio types
    TLKAUD_TYPE_CUSTOM_AUDIO,      // custom audio scene 1
    TLKAUD_TYPE_CUSTOM_AUDIO2,      // second custom audio scene (optional)

    TLKAUD_TYPE_MAX,
} TLKAUD_TYPE_ENUM;

步骤 2: 实现模块接口

创建新的模块实现文件 tlkmdi_custom_audio.c

/**********************************************************************
 * @file    tlkmdi_custom_audio.c
 * @brief   custom audio module implementation
 **********************************************************************/

#include "tl_common.h"
#include "tlkapi/tlkapi.h"
#include "tlkmw/tlkmw.h"

// module environment variables
static struct {
    uint8_t  enable;
    uint16_t handle;
} s_tlk_mdi_custom_audio_env = {0};

/**
 * @brief   initialize custom audio module
 */
static int tlkmdi_custom_audio_init(void)
{
    tmemset(&s_tlk_mdi_custom_audio_env, 0, sizeof(s_tlk_mdi_custom_audio_env));
    return TLK_ENONE;
}

/**
 * @brief   switch custom audio module state
 * @param   handle - connection handle
 * @param   status - TLK_STATE_OPENED/TLK_STATE_CLOSED
 * @return  true-success, false-fail
 */
static bool tlkmdi_custom_audio_switch(uint16_t handle, uint8_t status)
{
    if (status == TLK_STATE_OPENED) {
        s_tlk_mdi_custom_audio_env.enable = true;
        s_tlk_mdi_custom_audio_env.handle = handle;
        // TODO: initialize custom audio path

    } else {
        s_tlk_mdi_custom_audio_env.enable = false;
        // TODO: close custom audio path
    }
    return true;
}

/**
 * @brief   check if custom audio module is busy
 */
static bool tlkmdi_custom_audio_is_busy(void)
{
    return s_tlk_mdi_custom_audio_env.enable;
}

/**
 * @brief   start custom audio module
 * @param   handle - connection handle
 * @param   param - custom parameter (not used)
 * @return  TLK_ENONE-success, other-fail
 */
static int tlkmdi_custom_audio_start(uint16_t handle, uint32_t param)
{
    (void)param;
    if (s_tlk_mdi_custom_audio_env.enable) {
        return -TLK_EREPEAT;  // Prevent repeated startup
    }
    // TODO: start custom audio path
    return TLK_ENONE;
}
/**
 * @brief   close custom audio module
 * @param   handle - connection handle
 * @return  TLK_ENONE-success, other-fail
 */
static int tlkmdi_custom_audio_close(uint16_t handle)
{
    (void)handle;
    if (!s_tlk_mdi_custom_audio_env.enable) {
        return -TLK_EREPEAT;
    }
    s_tlk_mdi_custom_audio_env.enable = false;
    return TLK_ENONE;
}

/**
 * @brief   operate custom audio module
 * @param   handle - connection handle
 * @param   opcode - operation code (TLKAUD_OPCODE_XXX)
 * @param   pdata - operation data pointer
 * @param   dataLen - operation data length
 * @return  true-success, false-fail
 */
static bool tlkmdi_custom_audio_operate(uint16_t handle, uint8_t opcode, 
                                        uint8_t *pdata, uint16_t dataLen)
{
    (void)handle;
    (void)pdata;
    (void)dataLen;

    switch (opcode) {
    case TLKAUD_OPCODE_VOLUME_INC:
        // Handle the increase in volume
        return true;
    case TLKAUD_OPCODE_VOLUME_DEC:
        // Handle the decrease in volume
        return true;
    case TLKAUD_OPCODE_IS_SUPPORT_TONE_MIX:
        return false;  // Custom audio module does not support tone mixing
    default:
        return false;
    }
}

// module interface definition
const tlkapp_audio_modinf_t sTlkAppAudioCustomModinf = {
    .Init    = tlkmdi_custom_audio_init,
    .Switch  = tlkmdi_custom_audio_switch,
    .IsBusy  = tlkmdi_custom_audio_is_busy,
    .Start   = tlkmdi_custom_audio_start,
    .Close   = tlkmdi_custom_audio_close,
    .ToNext  = NULL,
    .ToPrev  = NULL,
    .operate = tlkmdi_custom_audio_operate,
};

步骤 3: 注册模块接口

tlkapp_audioModinf.c 中:

// 1. Add a header file at the top of the file
#if (TLK_CFG_CUSTOM_AUDIO_ENABLE)
extern const tlkapp_audio_modinf_t sTlkAppAudioCustomModinf;
#endif

// 2. Add the custom audio module interface to the spTlkAppAudioModinfs array
void tlkapp_audio_modinfNodeInit(void)
{
    // ... other module initializations

#if (TLK_CFG_CUSTOM_AUDIO_ENABLE)
    spTlkAppAudioModinfs[TLKAUD_TYPE_CUSTOM_AUDIO] = &sTlkAppAudioCustomModinf;
#endif
}

// 3. Add the custom audio module instance to the spTlkAppAudioModinfs array
#if (TLK_CFG_CUSTOM_AUDIO_ENABLE)
static const tlkapp_audio_modinf_t sTlkAppAudioCustomModinf = {
    .Init    = tlkmdi_custom_audio_init,
    .Switch  = tlkmdi_custom_audio_switch,
    .IsBusy  = tlkmdi_custom_audio_is_busy,
    .Start   = tlkmdi_custom_audio_start,
    .Close   = tlkmdi_custom_audio_close,
    .operate = tlkmdi_custom_audio_operate,
};
#endif

步骤 4: 设置默认优先级

tlkapp_audioScheduler.ctlkapp_audioScheduler_getDefaultPriority() 函数中添加:

static const uint8_t tlkapp_audioScheduler_priorityTab[TLKAUD_TYPE_MAX] = {
    [TLKAUD_TYPE_TONE]         = 7, 
    [TLKAUD_TYPE_CC_BT_VOICE]  = 5,
    [TLKAUD_TYPE_CC_BT_MUSIC]  = 2, 
    // ... other audio types

    // Add custom audio module default priority
    [TLKAUD_TYPE_CUSTOM_AUDIO] = 3,  // Medium priority
};

步骤 5: 发送开始/关闭事件

在适当的业务逻辑位置调用:

// Start custom audio module
int custom_audio_start(uint16_t handle)
{
    return tlkmdi_audio_sendStartEvt(TLKAUD_TYPE_CUSTOM_AUDIO, handle);
}

// Close custom audio module
int custom_audio_stop(uint16_t handle)
{
    return tlkmdi_audio_sendCloseEvt(TLKAUD_TYPE_CUSTOM_AUDIO, handle);
}

// A start with priority
int custom_audio_start_with_priority(uint16_t handle, uint8_t priority)
{
    return tlkmdi_audio_sendStartEvtEx(TLKAUD_TYPE_CUSTOM_AUDIO, handle, priority);
}

步骤 6: 测试验证

测试清单:

(1)基础功能测试

  • 能否正常启动自定义音频
  • 能否正常停止
  • 状态切换是否正确

(2)调度测试

  • 能否正确抢占低优先级任务
  • 能否被高优先级任务抢占
  • 恢复机制是否正常

(3)混音测试(如果支持)

  • 与 Tone 混音是否正常
  • 与 Music 混音是否正常

(4)优先级测试

  • 修改优先级后行为是否符合预期
  • 同优先级任务调度是否 round-robin

提示音(Tone)播放原理

Tone 模块概述

Tone(提示音)模块用于播放短音频提示,如:

  • 开机提示音
  • 来电铃声
  • 按键提示音
  • 电量低提示音

Tone 特点:

特性 说明
优先级 最高优先级(7),可抢占所有音频
播放方式 可独立播放或与 Music 混音
同步支持 TWS 模式支持双耳同步播放
资源占用 使用独立 Codec 资源

Tone 播放流程

 Tone Playback Flow

Tone 播放完整性保障机制

1. 忙状态检测

Tone 模块的忙状态检测同时检查两个条件:

bool tlkmdi_tone_is_busy(void)
{
    // 1. tone_is_playing(): Is the underlying tone playing
    //    - Check the status of the decoder
    //    - Check the status of the data buffer

    // 2. s_tlk_mdi_tone_ctx.waitSyncTimer: Is the TWS sync timer waiting
    //    - Used in TWS mode to wait for the peer device synchronization

    return tone_is_playing() || s_tlk_mdi_tone_ctx.waitSyncTimer;
}

2. 定时器回调机制

Tone 使用双回调机制确保播放完整性:

 Tone Dual-callback Mechanism

定时器回调代码:

void tlkmdi_tone_main(void)
{
    // 1. check TWS sync status
    bool isWait = tlkmdi_tone_waitSyncPlay();
    if(isWait){
        return;  // TWS sync timer is waiting, not playing
    }

    // 2. set next interrupt time
    tlkmdi_audio_task_set_next_irq(1500);  // 1.5ms

    // 3. execute Tone play
    tlkmdi_tone_player();
}

void tlkmdi_tone_main_loop(void)
{
    if (tlkmdi_tone_is_busy()) {
        return;  // still playing
    }
    // play complete, send close event
    tlkmdi_audio_sendCloseEvtEx(TLKAUD_TYPE_TONE, 0xffff, true);
}

3. TWS 同步机制

对于 TWS(True Wireless Stereo)场景,Tone 播放需要双耳同步:

static void tlkmdi_tone_waitSyncPlayStart(uint8_t tone_id)
{
    // request TWS sync play
    int ret = tlkmdi_audio_hostif_tone_requestSyncPlay(tone_id);
    if(ret == TLK_ENONE){
        // TWS sync request success, start wait timer
        // wait time: TLKMDI_TONE_WAIT_SYNC_TIMEOUT_MS = 150ms
        s_tlk_mdi_tone_ctx.waitSyncTimer = TLKMDI_TONE_WAIT_SYNC_TIMEOUT_MS / 10;
    } else {
        s_tlk_mdi_tone_ctx.syncTick = 0;
    } 
}

// TWS sync wait check
static bool tlkmdi_tone_waitSyncPlay(void)
{
    if (s_tlk_mdi_tone_ctx.waitSyncTimer == 0) {
        return false;  // no need to wait
    }

    if (s_tlk_mdi_tone_ctx.syncTick != 0) {
        // sync signal received
        return false;
    }

    // TWS sync wait timeout check in tlkmdi_tone_main()
    return true;
}

4. 播放完成通知链

 Tone Playback Completion Detection Chain

Tone 与 Music 混音

Tone 可以与 Music 混音播放,实现"背景音乐+提示音"效果。

1. 混音检测流程

bool tlkapp_audio_startTone(uint8_t fileIndex, uint8_t isActive)
{
    uint32_t isNeedCreateToneTask = 0xFFFF0000;  // default need create task

    const tlkapp_audioScheduler_node_t *nowTask = 
        tlkapp_audioScheduler_getRunningTask();

    if (nowTask != NULL) {
        // query current task if support tone mix   
        uint8_t opcode = TLKAUD_OPCODE_IS_SUPPORT_TONE_MIX;
        bool res = tlkapp_audio_modinfOperate(
            (uint16_t)nowTask->taskId, 
            (uint16_t)nowTask->info.optype, 
            &opcode, 1);

        if(res == true){
            // support tone mix, no need to create an independent task
            isNeedCreateToneTask = 0;
        }
    }

    uint32_t param = isNeedCreateToneTask | fileIndex;
    param |= (uint32_t)isActive << 8;

    return tlkapp_audio_modinfStart(TLKAUD_TYPE_TONE, TLK_INVALID_HANDLE, param);
}

2. 混音场景分析

场景 Music 支持混音 isNeedCreateToneTask 效果
播放 Tone Music 不支持混音 0xFFFF0000 抢占 Music,Tone 独占播放
播放 Tone Music 支持混音 0 Music 继续播放,Tone 混音输出
播放 Tone 无 Music 播放 0xFFFF0000 Tone 正常播放
TWS 同步 Tone TWS 不支持 0xFFFF0000 等待同步超时后本地播放

3. 混音实现

当支持混音时,Tone 数据与 Music 数据在音频输出层混合:

// in tlkmdi_tone_player(), mix tone with music 
static void tlkmdi_tone_player(void)
{
    uint16_t samples_num = 128;

    // clear tone output buffer
    memset(pcm16_tone, 0, sizeof(pcm16_tone));

    if (tone_is_playing()) {
        // get tone decoded samples
        tone_get_sample(pcm16_tone, samples_num * sizeof(tone_int), 
                        s_tlk_mdi_tone_ctx.sample_rate);

        // mix tone with music in output buffer
        // mix algorithm: pcm_out = pcm_music + pcm_tone (with overflow protection)
        for (i = 0; i < samples_num * 2; i++) {
            int32_t mixed = pcm_music[i] + pcm16_tone[i];
            // overflow protection
            if (mixed > 32767) mixed = 32767;
            if (mixed < -32768) mixed = -32768;
            pcm_output[i] = mixed;
        }
    }
}

附录

关键 API 速查

1. 调度器核心 API

// init audio scheduler
int tlkapp_audioScheduler_init(uint32_t cfg);

// task management
int tlkapp_audioScheduler_addTask(uint32_t taskId, tlkapp_audioScheduler_taskInfo_t info);
int tlkapp_audioScheduler_updateTask(uint32_t taskId, tlkapp_audioScheduler_taskInfo_t info, uint32_t sameDevTaskId);
int tlkapp_audioScheduler_pauseTask(uint32_t taskId);
int tlkapp_audioScheduler_resumeTask(uint32_t taskId);
int tlkapp_audioScheduler_deleteTask(uint32_t taskId);

// query task information
const tlkapp_audioScheduler_node_t *tlkapp_audioScheduler_getRunningTask(void);
const tlkapp_audioScheduler_taskInfo_t *tlkapp_audioScheduler_getTaskInfo(uint32_t taskId);
uint8_t tlkapp_audioScheduler_getCurOptype(void);

// scheduler task management
const tlkapp_audioScheduler_node_t *tlkapp_audioScheduler_SchPausedTask(bool isAutoSch, uint8_t audioType);
const tlkapp_audioScheduler_node_t *tlkapp_audioScheduler_roundRobin(void);

2. 模块接口 API

// module interface
bool tlkapp_audio_modinfSwitch(TLKAUD_TYPE_ENUM optype, uint16_t handle, uint8_t status);
int  tlkapp_audio_modinfStart(TLKAUD_TYPE_ENUM optype, uint16_t handle, uint32_t param);
int  tlkapp_audio_modinfClose(TLKAUD_TYPE_ENUM optype, uint16_t handle);
bool tlkapp_audio_modinfOperate(uint16_t handle, TLKAUD_TYPE_ENUM optype, uint8_t *pData, uint16_t dataLen);

3. 音频事件 API

// send audio event
int tlkmdi_audio_sendStartEvt(uint8_t audChn, uint16_t handle);
int tlkmdi_audio_sendStartEvtEx(uint8_t audChn, uint16_t handle, uint8_t priority);
int tlkmdi_audio_sendCloseEvt(uint8_t audChn, uint16_t handle);
int tlkmdi_audio_sendCloseEvtEx(uint8_t audChn, uint16_t handle, uint8_t isDelete);

音频通路介绍

Audio path 结构框图

Audio path 包含的模块:tlkapp/audio、tlkmw/audio 和 tlkmw/sys_dev/codec,框图如下所⽰:

  • tlkapp/audio 中包含了消息处理,module 接口,音频控制接口,音频任务调度逻辑
  • tlkmw/audio 中包含了各种音频场景的实现,如图中所示
  • tlkmw/sys_dev/codec 中包含了 codec 相关的代码

Audio path结构

Audio path 硬件模块

1) Timer

TIMER0 用作音频处理的主定时器

audio timer API 如下:

// 启动audio timer
void tlkmdi_audio_start_timer(void)
// 停止audio timer
void tlkmdi_audio_stop_timer(void)
// 设置audio timer的捕捉定时器
void tlkmdi_audio_set_timer(uint32_t cap_tick)
// 设置audio task的下一次中断时间
void tlkmdi_audio_task_set_next_irq(uint32_t tus)
// 设置和启动audio timer
void tlkmdi_audio_setup_and_start_timer(void)

2) CODEC

CODEC 包含 ADC(模数转换器)DAC(数模转换器):

  • 输入路径 (ADC):支持模拟麦克风 (AMIC)、数字麦克风(DMIC) 和线路输入 (LINEIN),可配置为单通道 (A1/A2/B1/B2) 或双通道 (A1_A2、B1_B2),支持 16-bit/24-bit 数据格式,采样率支持 16/44.1/48/96/192/384/768 kHz
  • 输出路径 (DAC):支持单通道或双通道输出,连接到耳机/扬声器
  • 主要初始化 API:

    // 上电 ADC/DAC
    void audio_codec0_power_on(audio_codec0_power_e power_mode, audio_codec0_volt_supply_e volt)
    // 配置 ADC 输入参数
    void audio_codec0_input_init(audio_codec0_input_config_t *input_config)
    // 配置 DAC 输出参数
    void audio_codec0_output_init(audio_codec0_output_config_t *output_config)
    // ADC 模拟/数字增益
    void audio_codec0_set_input_again/dgain(audio_codec0_input_select_e input, audio_codec0_input_again_e gain)
    // DAC 模拟/数字增益
    void audio_codec0_set_output_again/dgain(audio_codec0_output_select_e output, audio_codec0_output_again_e gain)
    

3) DMA

DMA 用于音频数据的高效搬运,无需 CPU 干预:

  • RX DMA (如 DMA0):从 FIFO 搬运音频数据到内存缓冲区 gpTlkDrvCodecMicBuffer
  • TX DMA (如 DMA1):从内存缓冲区 gpTlkDrvCodecSpkBuffer 搬运数据到 FIFO 供 DAC 输出

4) FIFO

FIFO 作为音频数据流的中间缓冲:

  • RX FIFO (如 FIFO0):接收 CODEC ADC 或 I2S 的数据
  • TX FIFO (如 FIFO1):向 DAC 或 I2S 发送数据

Audio path 各音频场景目录结构

Audio path目录结构

  • Audio path 代码在 tlkmw/audio 目录中,包含了各种不同音频场景的应用
  • a2dp source 包含了 A2DP source 功能,音源可以来自本地正弦波、linein 或 UAC
  • a2dp to bis 包含 A2DP in LE BIS out 功能
  • ANC 包含 ANC 场景下的音频应用,有 BT music、BT voice 以及 TPSLL audio
  • bt_audio 中包含 BT A2DP sink 和 BT voice 的相关功能
  • dongle 中包含 USB to headset 的功能,有 BT voice 和 A2DP source 两条通路
  • hra_audio 包含助听器的功能,有纯助听通路、BT music 通路、BT voice 通路
  • interphone 包含 mesh 头盔应用相关功能
  • le_audio 包含 BLE CIS 和 BLE BIS 功能
  • ll_audio 包含 TPSLL audio 耳机端的功能
  • ll_dongle 包含 TPSLL audio dongle 端的功能
  • recording_card 包含录音卡应用相关功能
  • tone 包含提示音功能
  • common 中包含各音频场景公用的逻辑,比如 Timer 的管理,内存的分配,DSP 通路的代码

common 菜单

这是 tlkmw/audio/common 目录,里面包含了 D25F 上 DSP 的驱动代码,目前只有 BT voice 和 TPSLL voice 上行通路用到了 DSP,tlkmdi_audmem.c 中包含了音频通路和算法用到的内存分配功能,tlkmdi_audio_common.c 中包含 audio irq task, main loop, dsp main loop 等各音频场景公共功能,host_interface 文件夹中包含和 stack 交互的逻辑。

Audio path MDI 接口介绍

Application 和 middle ware 之间的接口名称为 MDI,所有 audio task 共用一套 audio 控制逻辑,每个 task 把接口注册到 Application 中,Application 通过 type 区分不同 task。

typedef struct
{
    int (*Init)(void);
    bool (*Switch)(uint16_t handle, uint8_t status);
    void (*Timer)(void);
    bool (*IsBusy)(void);

    int (*Start)(uint16_t handle, uint32_t param);
    int (*Close)(uint16_t handle);
    bool (*ToNext)(void);
    bool (*ToPrev)(void);
    bool (*operate)(uint16_t handle, uint8_t opcode, uint8_t *pdata, uint16_t dataLen);
} tlkapp_audio_modinf_t;

这是 tlkapp/audio 中 module interface 的接口定义,各种音频场景定义不同的接口,如下是 BT music 场景的接口定义。

tlkapp/audio 通过调用 Switch 进入或退出音频场景,通过 Start、Close、ToNext、ToPrev、operate 接口调用音频场景控制接口。

static const tlkapp_audio_modinf_t sTlkAppAudioCCBTMusicModinf = {
    .Init    = tlkmdi_bt_music_init,
    .Switch  = tlkmdi_bt_music_switch,
    .IsBusy  = tlkmdi_bt_music_is_busy,
    .Start   = tlkmdi_bt_music_start,
    .Close   = tlkmdi_bt_music_close,
    .ToNext  = tlkmdi_bt_music_next,
    .ToPrev  = tlkmdi_bt_music_previous,
    .operate = tlkmdi_bt_music_operate,
};

以上是 BT music 场景注册的接口。

    bt_music_audio_path_init();
    bt_audio_register_get_pcm_data_callback(bt_music_get_playback_data);
    tlkmdi_audio_register_cb(TLKMDI_AUDIO_CB_TIMER,bt_audio_main);
    tlkmdi_audio_register_cb(TLKMDI_AUDIO_CB_MAIN,bt_audio_main_loop);
    bt_audio_task_register_run_cb(NULL, 1);

以上是 tlkmdi_bt_music_switch 主要的逻辑,初始化内存和算法、裸机项目注册 timer irq 中断处理函数,RTOS 项目注册 audio irq task 事件处理函数,注册 main loop 处理函数。

耳机端音频通路

如下图是 cc-headset 工程中 MIC 上行通路和 speaker 下行通路

  • MIC 上行通路会用到多个算法,除了 NN_NS 运行在 DSP 中,其他算法都在 D25F 上运行,默认只开启了 NN 算法
  • speaker 下行通路 ASRC 是 44.1K 到 48K 的重采样算法,TL751x 是硬件 ASRC,其他芯片需要软件实现

耳机端音频通路

Dongle 端音频通路

1. Dongle⾳频通路⼯作⽅式

Dongle 在插⼊ USB 主机后会枚举成⼀个 USB 麦克风设备和⼀个 USB 扬声器设备。dongle 正常⼯作时,将来⾃⽿机的⾳频处理后通过 USB 麦克风通路上传给 USB 主机,将来⾃ USB 扬声器通路的⾳频数据处理后发送给⽿机。

2. Dongle ⾳频通路流程

Dongle ⾳频通路

(1)Dongle 接收到来⾃⽿机的单通道 16kHz 采样率 LC3 编码的 MIC 数据包

(2)在 MIC 任务中主要对 MIC 数据进⾏解码、数据对⻬、采样率转换和⾳量调整

(3)USB iso in 处理函数将处理后的数据发送给 USB 主机

(4)USB iso out 处理函数接收到来⾃USB 主机的 48kHz 采样率的 PCM 格式的 speaker 数据

(5)在 audio 任务中主要对 speaker 数据进⾏⾳量调整、数据对⻬、编码成 LC3 格式

(6)将 LC3 编码的 speaker 数据包发送给⽿机

混音逻辑

CC-TWS 和 CC-headset 工程支持双模混音功能,BT music 和 TPSLL audio 混音功能以及 BT voice 和 TPSLL audio 混音功能默认开启,可以通过宏 LE_AUDIO_BT_MUSIC_MIX_ENABLE 或 LE_AUDIO_BT_VOICE_MIX_ENABLE 来控制混音功能开启/关闭,宏位于 tlkmw/audio/audio_mw_manager.h 中,音频格式是 48K,24-bit。

BT music 时调用 mix_ll_audio_stereo API 和 TPSLL 音频进行混合。

/**
 * @brief      Mix asynchronous audio and Bluetooth music in async_audio bt_music mix mode.
 * @param[in]  p0 - Pointer to first data buffer.
 * @param[in]  p1 - Pointer to second data buffer.
 * @param[in]  samples_num - Number of samples to mix.
 * @return     Number of mixed samples.
 */
uint16_t mix_ll_audio_stereo(int16_t *p0, int16_t *p1, uint16_t samples_num);

CODEC 接口介绍

CODEC 代码在 tlkmw/sys_dev/codec 目录下,tlkdrv_codec.c 是抽象层,speaker buffer 和 mic buffer 定义在这里,其他是不同芯片的 CODEC 驱动。

CODEC 接口

const tlkdrv_codec_modinf_t gcTlkDrvIcodecInf = {
    .IsOpen = tlkdrv_icodec_isOpen,
    .Init   = tlkdrv_icodec_init,
    .Open   = tlkdrv_icodec_open,
    .Close  = tlkdrv_icodec_close,
    .Config = tlkdrv_icodec_config,
};

CODEC 驱动通过 code mod interface 注册到抽象层,应用通过 tlkdrv_open_codec()打开 CODEC, 通过 tlkdrv_codec_fillSpkBuff()把 PCM 数据写入 CODEC 的 speaker buffer, 通过 tlkdrv_codec_readMicData()获取 MIC 数据。

音频算法简介

算法概览

本 SDK 内主要使用了以下音频算法:

  • SBC ( Sub-band Coding ):子带编码
  • mSBC ( Modified Sub-band Coding ):改进的子带编码
  • CVSD ( Continuous Variable Slope Delta Modulation ):连续变量增量调制
  • AAC ( Advanced Audio Coding ):高级音频编码
  • LC3 ( Low Complexity Communication Codec ):低复杂度通信编解码器
  • LC3p ( Low Complexity Communication Codec Plus ):低复杂度通信编解码器增强版
  • Opus ( Opus Interactive Audio Codec ):交互式音频编解码器
  • EQ ( Equalizer ):均衡器
  • BF ( Beamforming ):波束成形
  • AEC ( Acoustic Echo Cancellation ):回声消除
  • NS ( Noise Suppression ):降噪
  • AGC ( Automatic Gain Control ):自动增益控制
  • PLC (Packet Loss Concealment):丢包补偿
  • ASRC (Asynchronous Sample Rate Conversion):异步采样速率转换

算法通路框图

以 BT/TPSLL 双模应用为例,根据音频通路的不同运行状态,将音频算法区分为如下图所示的三种模式:

Voice uplink 为语音上行通路,其中 BT 支持 mSBC 和 CVSD 两种编码算法,TPSLL 支持 LC3 和 LC3p 两种编码算法,语音算法通路为 BF->NN_NS/ANS->AEC->AGC,降噪算法提供神经网络降噪(NN_NS)和传统降噪(ANS)两种可选。

Voice downlink 为语音下行通路,其中 BT 支持 mSBC 和 CVSD 两种解码算法,TPSLL 支持 LC3 和 LC3p 两种解码算法,PLC 为对应解码的丢包补偿算法,ASRC 用于音频数据的采样率转换,旨在匹配混音时 BT 和 TPSLL 两条音频通路的数据流。

Music downlink 为音乐下行通路,其中 BT 支持 mSBC 和 CVSD 两种解码算法,TPSLL 支持 LC3 和 LC3p 两种解码算法,ASRC 算法同上,EQ 用于调节音效。

算法通路框图

算法接口

为方便各音频算法的统一管理,SDK 内抽象出了一套标准算法接口,便于算法调用和新算法介入,具体接口代码如下所示:

typedef struct {
    /* Reset algorithm parameter settings */
    uint8_t(*audio_alg_param_reset)(void);

    /* Set algorithm parameter settings */
    uint8_t(*audio_alg_param_set)(uint8_t type, void *param);

    /* Get the memory size used by the algorithm */
    uint16_t(*audio_alg_get_size)(uint8_t channel);

    /* Init channel: mono or stereo */
    int8_t(*audio_alg_init)(uint8_t *p_buff, uint8_t channel);

    /* deinit */
    int8_t(*audio_alg_deinit)(void);

    /* Alg process */
    int (*audio_alg_process)(uint8_t *ps, uint8_t *pd, uint16_t len, uint8_t width, uint8_t channel);
} audio_alg_interface_t;

定义结构体数组将算法集中放置,根据具体的算法索引获取到具体的算法接口内容,算法类型如下枚举变量所示:

const audio_alg_interface_t audio_alg_if[ALG_TYPE_MAX] = {
    //alg1
    //alg2
    //...
}

typedef enum {
    ALG_AAC_DEC = 0,
    ALG_SBC_ENC,
    ALG_SBC_DEC,
    ALG_LC3_ENC,
    ALG_LC3_DEC,
    ALG_CVSD_ENC,
    ALG_CVSD_DEC,
    ALG_MSBC_ENC,
    ALG_MSBC_DEC,
    ALG_LC3_PLUS_ENC,
    ALG_LC3_PLUS_DEC,
    ALG_OPUS_ENC,
    ALG_OPUS_DEC,
    ALG_LHDC_DEC,
    ALG_EQ,
    ALG_ADPCM,
    ALG_ASRC,
    ALG_DRC,
    ALG_AEC,
    ALG_ENC,
    ALG_ANS,
    ALG_SPK_ANS,
    ALG_HYBRID,
    ALG_AGC,
    ALG_ASRC_48TO16_16BIT,
    ALG_ASRC_48TO16_24BIT,
    ALG_ASRC_16TO48_16BIT,
    ALG_ASRC_16TO48_24BIT,
    ALG_ASRC_48TO441,
    ALG_ASRC_441TO48_16BIT,
    ALG_ASRC_441TO16_16BIT,
    ALG_ASRC_16TO441_16BIT,
    ALG_ASRC_48TO32_16BIT,
    ALG_ASRC_32TO48_16BIT,
    ALG_ASRC_32TO16_16BIT,
    ALG_PPM_SPK,
    ALG_PPM_MIC,
    ALG_PPM_TWS_SPK,
    ALG_LC3_24BIT_ENC,
    ALG_LC3_24BIT_DEC,
    ALG_VAD,
    ALG_PPM_SPK_24BIT,
    ALG_NN_NS,
    ALG_NN_NS_VAD,
    //ALG_PPM_MIC_24BIT,
    ALG_DEFAULT,
    ALG_TYPE_MAX
} audio_alg_type_e;

以 SBC 解码算法为例,介绍一下算法在 SDK 内的使用流程:

(1) 算法注册:将该算法使用到的接口注册到前述定义的结构体数组中

const audio_alg_interface_t audio_alg_if[ALG_TYPE_MAX] = {
#if TLKALG_SBC_DEC_ENABLE
    [ALG_SBC_DEC] = {
        .audio_alg_get_size = tlkalg_sbc_dec_get_size,
        .audio_alg_init = tlkalg_sbc_dec_init,
        .audio_alg_deinit = tlkalg_sbc_dec_deinit,
        .audio_alg_process = tlkalg_sbc_dec_process,
    },
#endif
}

(2) 算法初始化:根据算法所需内存大小,在堆上申请对应的空间,基于该地址进行算法初始化操作

audio_alg_interface_t *p_audio_alg_if = audio_alg_get_interface_by_type(ALG_SBC_DEC);

if (s_alg_sbc_dec_buffer == NULL) {
    uint16_t sbc_dec_mem_size = p_audio_alg_if->audio_alg_get_size(ALG_CHANNEL_STEREO);
    s_alg_sbc_dec_buffer      = (uint8_t *)tlkalg_malloc_func(sbc_dec_mem_size);
    if (s_alg_sbc_dec_buffer == NULL) {
        tlkapi_printf(APP_LOG_EN, "sbc dec buff alloc failed");
        return false;
    }
    p_audio_alg_if->audio_alg_init(s_alg_sbc_dec_buffer, ALG_CHANNEL_STEREO);

    tlkapi_printf(APP_LOG_EN, "sbc_dec_mem_size: %d", sbc_dec_mem_size);
}

(3) 算法处理:在对应的音频通路位置,调用算法处理接口,buf_ptr 指向待处理数据,p_des 指向处理后数据,其他为算法配置参数

audio_alg_interface_t *audio_alg_if_handle = audio_alg_get_interface_by_type(ALG_SBC_DEC);
ret = audio_alg_if_handle->audio_alg_process(buf_ptr, (uint8_t *)p_des, bt_music_cfg.sbc_framesize, ALG_WIDTH_16, ALG_CHANNEL_STEREO);

(4) 算法复位:在算法使用场景结束之后,将算法参数还原,并将使用的内存空间释放掉

audio_alg_interface_t *p_audio_alg_if = audio_alg_get_interface_by_type(ALG_SBC_DEC);
if (s_alg_sbc_dec_buffer != NULL) {
    tlkalg_free_func(s_alg_sbc_dec_buffer);
    p_audio_alg_if->audio_alg_deinit();
    s_alg_sbc_dec_buffer = NULL;
}

音量调节

通话场景

本 SDK 在通话场景下的音量通过 bt_audio_control_voice_volume()函数调节。

在通话场景下,对传入的音频数据逐个 sample 进行音量调节。当前音频音量小于配置音量比例时,会提高音频音量;当前音频音量大于配置音量比例时,会降低音频音量。流程如下图所示:

通话场景

音乐场景

本 SDK 在音乐场景下的音量通过 bt_audio_control_music_volume()函数调节。

在音乐场景下,当需要进行音量调节的 sample 数达到一定数量(由宏 SAMPLES_NUM_CHANGE_VOLUME 控制,目前为 10)时,才会进行相应的音量调节。当前音频音量小于/大于配置音量比例时,会累计 1 次音量调节数,当音量调节数为 SAMPLES_NUM_CHANGE_VOLUME 时,音量调节数清零并进行相应的音量调节。流程如下图所示:

音乐场景

提示音播放

提示音制作

提示音音源格式:16kHz 单声道

提示音制作工具:wave_to_tone_bin_tool_v1.0.1

提示音制作步骤:

根据 tone.h 定义的 type, 把⾳源改成 1,2,3…命名,如下是 type 和⾳源的对应关系。修改好⾳源名字后⽤pcm16to8_vol_1024.bat 脚本⽣产 tone ⽂件,⽣产的⽂件是 ADPCM 格式。

typedef enum
{
    TONE_PAIRING = 0,   ---> wav file 1
    TONE_CONNECTED,     ---> wav file 2
    TONE_LE_CONNECTED,
    TONE_LOW_POWER,
    TONE_RING,
    TONE_POWER_OFF,
    TONE_DISCONNECTED,
    TONE_BT_AUDIO_MUSIC_MODE,  ---> wav file 9
    TONE_BT_AUDIO_GAME_MODE,  ---> wav file 10

    MODE_MAX_TONE
} e_type_tone_t;

提示音下载

制作得到的提示音文件可下载在 flash 中,具体下载地址由宏 CONFIG_TLK_AUDIO_TONE_DOWNLOAD_ADDR定义。

#ifndef CONFIG_TLK_AUDIO_TONE_DOWNLOAD_ADDR
#define CONFIG_TLK_AUDIO_TONE_DOWNLOAD_ADDR (FLASH_R_BASE_ADDR + 0X1A0000)
#endif

#ifndef FLASH_R_BASE_ADDR
#define FLASH_R_BASE_ADDR (0x20000000)
#endif

提示音添加

提示音的编码格式有 SBC 和 ADPCM 两种,其播放流程一致。以 ADPCM 编码的提示音为例,调⽤ tone_play()指定 id, 如 tone_play(CONNECTED_TONE),程序会初始化 ADPCM,播放提示音。

在 BT audio 播放逻辑或 TPSLL audio 播放逻辑中调⽤tone_get_sample()获取 PCM 数据。

DSP 使用

DSP 的功能通过宏 TLK_MW_DSP_COMM_ENABLE控制,使用时可在对应 demo 的 app_config.h 中开启该宏。

#define TLK_MW_DSP_COMM_ENABLE      1

DSP 默认 RAM boot 方式,由 D25F 搬运 DSP bin 的代码和数据, DSP reset vector 地址为:DSP_FW_DOWNLOAD_FLASH_ADDR,DSP 的 bin 文件默认烧录到 0x200000 地址。

#define DSP_FW_DOWNLOAD_FLASH_ADDR      0x2100660

DSP 与 D25F 双核通信机制

DSP 和 D25F 之间通过 mailbox 机制传递消息(ipc message)+share memory 传递 audio 数据,如下图所示:

双核通信机制

其中,mailbox 长度为 2 words,2 字节为消息头,包含消息类型、有效数据长度以及校验位,消息类型可分为:control message 和 audio data process message;6 字节为有效信息位,根据消息类型不同进行定义;DSP 数据传输 path 共有 3 条,可用于 audio 数据、算法参数等。

typedef enum
{
    IPC_DATA_PATH_0,
    IPC_DATA_PATH_1,
    IPC_SET_PARAM_PATH_2,
    IPC_DATA_PATH_MAX,
} ipc_data_path_e;

ipc/audio data buff 开在 share memory 中,大小为 20 KB,地址在 DSP 的 DRAM 中,定义了不同 data path 的读写指针、算法类型、数据长度等信息。

DSP 在 mailbox 中断函数中接收 D25F 的 ipc message,写入 queue 中,并在 mainloop 中进行 message process。

DSP 使用流程

基于上述 DSP 与 D25F 双核通信机制,DSP 可在不同的应用场景下进行动态开关,DSP 使用流程为:

在 audio 模块初始化时调用 DSP 初始化函数,进行 ipc/audio data buff 的分配、初始化,以及 DSP boot、DSP start、DSP mailbox 函数使能; D25F 帮 DSP 完成 boot 之后,DSP 处于 TLKDRV_DSP_STATE_BOOTING 状态。

void tlkmw_dsp_init(void)
{
    d25f_init_ipc_buffer();
    tlkdrv_dsp_init();
}

DSP 开始运行,并主动给 D25F 发送 handshake done 的消息,D25F 收到该消息之后,关闭 DSP 时钟,DSP 进入 TLKDRV_DSP_STATE_PAUSED 状态。

需使用 DSP 的音频场景下,调用 tlkmw_dsp_resume()函数开启 DSP 时钟,DSP 进入 TLKDRV_DSP_STATE_RUNNING。

ipc_msg_register_data_process_done_cb()用于注册处理 DSP 返回数据的回调函数。

d25f_send_audio_data_to_dsp()函数用于将 MCU 侧的待处理数据写入 share memory,并触发 DSP mailbox 中断。

void tlkmw_dsp_resume(void)
{
    tlkdrv_dsp_resume();
}

切出 DSP 使用场景后,调用 tlkdrv_dsp_pause()函数使 DSP 进入 TLKDRV_DSP_STATE_PAUSING 状态。在 OS 的 DSP timer 中,DSP 最终切换为 TLKDRV_DSP_STATE_PAUSED 状态,并关闭 DSP 时钟。

void tlkdrv_dsp_pause(void)
{
    if(sTlkdrvDspState != TLKDRV_DSP_STATE_RUNNING){
        return;
    }
    sTlkdrvDspState = TLKDRV_DSP_STATE_PAUSING;
    tlksys_timer_reStart(TLKSYS_TASKID_AUDIO,&sTlkdrvDspTmr);
}

协议栈及 profile 相关

BT 应用相关

Scan 管理

(1) Scan 简介

该章节主要介绍 Classic Bluetooth(以下统称 BT)scan 的基本概念以及 SDK 中 scan 的原理和使用方法。

BT 的 Scan 是指在无连接状态下,设备周期性跳频监听周围终端发出的 Inquiry(查询)报文,以发现可配对设备并收集其关键信息(蓝牙地址、设备类别、时钟偏移、页面扫描模式等)。该过程仅用于建立连接前的发现阶段,与 BLE 的“广播—扫描”概念不同,不涉及广播包收发。

两种主要使用方式:

  • Inquiry Scan(查询扫描)

Peripheral 设备(如耳机、音响等)周期性的开窗监听 central 设备(如手机、笔记本)发出的 GIAC/DIAC 查询。被查询到后返回 FHS 包,供 central 获取地址与时钟,随后可进入 page 流程建立 ACL 链路。

  • Page Scan(寻呼扫描)

若 peripheral 允许被连接,则在查询完成后继续周期性地扫描 page 报文;central 利用前期获得的时钟偏移快速寻呼,完成链路建立。

应用场景

  • 首次配对:手机发起 Inquiry 后发现耳机,用户选择要连接的耳机发起 page 随后触发配对/绑定。
  • 重新连接:已绑定设备直接利用地址发起 page,无需再次 inquiry。当耳机与手机首次配对成功后,手机在配对列表中保存了耳机的蓝牙设备地址及链路密钥。后续若耳机重新开机或进入可连接范围,用户仅需在手机端点击已有耳机设备即可跳过 inquiry 阶段,直接发起 page 流程。

简而言之,BT Scan 是“查询-响应”机制:peripheral 端开启 Inquiry/Page Scan,central 端发出 Inquiry 完成设备发现,随后通过 page 建立链路,实现后续配对、认证与 profile 连接。

(2) Scan 使用

Scan 使用

/**
 * @brief       This function is called by the application to set the Bluetooth scan mode.
 * @param[in]   scan_value - the scan mode to be set.
 * @param[in]   time       - time duration, if scan_value is TLKMDI_BTSCAN_MODE_BOTH_DISABLE time will be neglect. Unit:S.
 *                           If value is not TLKMDI_BTSCAN_MODE_BOTH_DISABLE and time is set as 0, the scan will always on.
 *                           If time is set as 0xFFFF, then only update the scan value, timeout continues to count down.
 * @return      none.
 * @note        If the scan mode is already set to the same value, the function returns mode immediately. If the scan mode is different,
 *              the function sends a HCI command to set the scan mode. If the HCI command is sent successfully, the function sets a timer
 *              to wait for the HCI command complete event. If the HCI command fails, the function sets the scan mode as pending and
 *              waits for the next scan mode change. If the scan mode is set to TLKMDI_BTSCAN_MODE_BOTH_DISABLE, the function stops the timer.
 *              If the scan mode is set to a value other than TLKMDI_BTSCAN_MODE_BOTH_DISABLE, the function starts the timer with the specified time.
 */
void tlkmdi_btSet_scan(uint8_t scan_value, uint16_t time)

Scan 管理模块的关键函数,主要用于设置当前 scan 模式和 scan 超时时间。调用该函数后会向 BT 的控制器发送一条 HCI_Write_Scan_Enable HCI 指令。

特别注意:该函数非线程安全,不允许在中断中调用。RTOS 环境下,如果在 audio、system 或 user 等其他非本线程任务中调用该接口,需要通过消息队列机制发送至 host 线程保障安全执行。

#define TLKMDI_BTSCAN_TIME_UNIT 1000000 // 1s
scan 超时时间 = time * TLKMDI_BTSCAN_TIME_UNIT  // uinit:s

tlkmdi_btSetScan_timer 的 interval,该定时器每 1s 被触发一次,其超时时间由 tlkmdi_btSet_scan 的第二个参数 time 决定。

typedef enum
{
    TLKMDI_BTSCAN_MODE_BOTH_DISABLE,
    TLKMDI_BTSCAN_MODE_INQUIRY_SCAN,
    TLKMDI_BTSCAN_MODE_PAGE_SCAN,
    TLKMDI_BTSCAN_MODE_BOTH_SCAN,
} TLKMDI_BTSCAN_ACTIVE_MODE_ENUM;

TLKMDI_BTSCAN_ACTIVE_MODE_ENUM 用于描述 scan 模式,当前提供了Both scan(Inquiry Scan + Page Scan)、Inquiry Scan 、Page Scan 和关闭 scan 三种模式。除 TLKMDI_BTSCAN_MODE_BOTH_DISABLE 外,若需将选定模式设为长期有效,可把对应接口的 time 形参置 0,直至显式关闭或重新切换模式。

typedef struct
{
    uint8_t         cur_scan_state;
    uint8_t         waiting_confirm_value : 4; /* waiting hci set scan command complete */
    uint8_t         waiting_flag          : 2; /* waiting hci set scan command complete */
    uint8_t         pending_scanflag      : 2;
    uint16_t        waiting_confirm_value_time;
    uint8_t         pending_scan_value;        /* during waiting hci set scan command complete, system set a new value */
    uint8_t         reserve[3];
    uint16_t        pending_scan_value_time;
    uint16_t        timeout;                   /* the number of 1s unit */
    TlkApiTimer_t   timer;
} tlkmdi_bt_scan_t;

tlkmdi_bt_scan_t 描述了 scan 状态机上下文,兼顾“立即生效”与“异步等待 HCI 完成”两种场景。任何扫描请求首先更新 waiting_confirm_value 域并下发 HCI。tlkmdi_btSetScan_hciCmdEvt_cb 触发后把 waiting_confirm_value 写入 cur_scan_state,再检查 pending_scanflag,若有则立即发起新一轮 HCI 设置。在等待期间收到的新模式不会丢失,而是存入 pending_scan_value,确保“最后一条配置”最终生效。当 timeout 减至 0 时,内部自动切换为 TLKMDI_BTSCAN_MODE_BOTH_DISABLE,并通过注册回调通知应用层。

(3) API 介绍

void tlkmdi_btScan_process_init(void);

该函数用于完成 scan 系统的底层初始化。创建一个定时器,周期由宏 TLKMDI_BTSCAN_TIME_UNIT 定义(通常为 1 s),回调注册为 tlkmdi_btSetScan_timer。

该定时器后续负责递减扫描超时计数、触发自动关闭逻辑,以及重试 pending 请求,是整个“超时-自停”机制的时间基准。

int tlkmdi_btSetScan_hciCmdEvt_cb(uint8_t *pData, uint16_t dataLen);

该函数是 HCI_Write_Scan_Enable 命令的完成事件回调,负责把“异步结果”变更为“正式状态”,并处理可能存在的“pending 请求”链。

  • 校验状态

    • 若 status == BTH_HCI_ERROR_NONE 且本端确实处于等待态(waiting_flag ≠ 0),立即将 waiting_confirm_value 提升为当前有效模式 cur_scan_state。新模式为 TLKMDI_BTSCAN_MODE_BOTH_DISABLE 时,直接停止定时器并清零超时计数;否则重启定时器,若上层曾指定 time = 0xFFFF(仅更新模式、不重置时长),则保留原倒计时,否则按新下发的 waiting_confirm_value_time 重新装载。
  • 清理等待域

    • 无论成功失败,一次命令结束即清零 waiting_flag 及相关备份值,保证下一轮设置可用。
  • 链式触发

    • 如果在等待期间又有新请求被缓存(pending_scanflag ≠ 0),立即尝试下发对应 HCI 命令;发送成功则再次进入等待态,失败则打印错误并丢弃缓存。由此实现连续配置不丢包且最终状态一致的可靠策略。
void tlkmdi_btSet_scan(uint8_t scan_value, uint16_t time);

该函数用于动态切换 scan 模式。通过设定目标模式与持续时间,函数在底层完成“立即生效”、“异步等待 HCI 完成”和“超时自动关闭”三件事件:

  • 若请求模式与当前已生效模式相同,直接返回,不产生额外操作。

  • 若不同,立即下发 HCI 命令,同时把期望模式、超时值更新到 tlkmdi_bt_scan_t 控制块的等待域;命令成功送出后启动定时器,在 1 s 周期内递减剩余时长并监听 BTH_EVTID_SET_SCAN_CMD_COMPLETE 事件。

  • 在等待 HCI 回包期间收到的新请求会被缓存,待当前命令完成后再自动发起下一轮设置,保证“最后一次配置”一定被执行。

  • 特殊时间参数:当time 为0时,持续时间永久有效,直到下一次显式调用;当time 为 0xFFFF 时,仅更新模式值,不重置内部倒计时,用于“续期”场景。

当内部倒计时减至 0 时,模块自动关闭扫描并通过注册回调通知应用,从而实现“一键设置、超时即停”的完整闭环。

uint8_t tlkmdi_btGetScan_state(void);

获取当前的 scan 状态。当 waiting_flag被置位,返回当前的 scan 模式,参考 TLKMDI_BTSCAN_ACTIVE_MODE_ENUM;当 pending_scan_value_time不为0,返回当前剩余的时间;以上两个条件都不满足,则返回 pending_scan_value_time 的值。

uint16_t tlkmdi_btscan_getRemainedScanTime(void);

获取当前剩余的 scan 时间。

uint8_t tlkmdi_btscan_getCurScanState(void); 

获取当前的 scan 模式,参考 TLKMDI_BTSCAN_ACTIVE_MODE_ENUM。

回连管理

(1) 回连概览

BT 回连机制发生在设备开机上电或 ACL 链路超时掉线时。SDK 采用主动回连和被动等待的策略:先以 page 方式持续寻呼目标设备 10 s,未果则切入 page scan 状态 2 s 以接受对端寻呼;两者合计 12 s 记为一次 retry。上电回连默认 retry 上限 3 次,总超时 36 s。任一次 page 或 page scan 阶段成功建立链路会立即终止回连流程并退出回连模式。

#ifndef TLKMDI_BTRECON_RETRY_NUM_POWERON
#define TLKMDI_BTRECON_RETRY_NUM_POWERON        3       // power on default retry number
#endif

#ifndef TLKMDI_BTRECON_RETRY_NUM_LINK_LOSS
#define TLKMDI_BTRECON_RETRY_NUM_LINK_LOSS      25      // ACL link loss(ACL-8) default retry number
#endif

Reconnection Timeout

(2) 回连使用

#define TLKMDI_BTRECON_CHECK_INTERVAL 5000 

回连状态机状态检测 timer 的 interval,默认 5 ms。状态机(tlkmdi_btRecon_check_timer)的运作原理下文中会详细讲解。

#define TLKMDI_BTRECON_PAGE_INTERVAL  10000000 

回连流程管理 timer 的 interval,默认 10 s。timer(tlkmdi_btRecon_timer)的运作原理下文中会详细讲解。

#define TLKMDI_BTRECON_INTERVAL_TIME  2000000

回连流程中一次 page 结束后,page scan 的持续时间,默认 2 s。

typedef enum
{
    TLKMDI_BTRECON_STATE_IDLE = 0,
    TLKMDI_BTRECON_STATE_START,
    TLKMDI_BTRECON_STATE_PAGE,

    TLKMDI_BTRECON_STATE_WAIT_PAGE_CANCEL,

    TLKMDI_BTRECON_STATE_INTERVAL,
    TLKMDI_BTRECON_CANCEL_SCAN,
    TLKMDI_BTRECON_STATE_WAIT_PROFILE,

    TLKMDI_BTRECON_STATE_WAIT_STOP,
} TLKMDI_BTRECON_STATE_ENUM;

TLKMDI_BTRECON_STATE_ENUM 用于描述回连状态机的完整生命周期,各状态含义如下:

状态 说明
TLKMDI_BTRECON_STATE_IDLE 回连空闲态。上电初始化、用户手动取消回连或回连成功会后停留于此,等待应用层触发。
TLKMDI_BTRECON_STATE_START 回连已触发,开始获取配对列表、初始化计时与资源,准备进入 page。
TLKMDI_BTRECON_STATE_PAGE 主动 page 阶段。持续对目标设备发起 page,最长 10 s;成功则跳转至 WAIT_PROFILE,超时则进入 WAIT_PAGE_CANCEL。
TLKMDI_BTRECON_STATE_WAIT_PAGE_CANCEL 正在取消本次 page。下发 HCI 取消命令后需等待 Command Complete,确保硬件退出 page。
TLKMDI_BTRECON_STATE_INTERVAL page scan 阶段。停止 page 并进入 page scan 2 s,允许对端反向寻呼;时间到即完成一次 retry。
TLKMDI_BTRECON_CANCEL_SCAN 终止 page scan 状态。收到停止回连请求时,若正处于 page scan ,需等待退出完成。
TLKMDI_BTRECON_STATE_WAIT_PROFILE ACL 已建立,等待上层 profile(A2DP/AVRCP/HFP)完成连接与配置;成功后终止回连,失败则重试或降级。
TLKMDI_BTRECON_STATE_WAIT_STOP 收到强制停止指令,所有链路已断开,等待底层事件确认完全退出,最终返回 IDLE。

状态机按 IDLE、PAGE、WAIT_PAGE_CANCEL、INTERVAL 、START 循环,最多 retry_num 次;任一阶段连接成功或应用强制停止,即提前跳转至 WAIT_PROFILE 或 WAIT_STOP,确保回连过程可控、可中断、资源及时释放。

typedef struct
{
    uint8_t         retry_num;
    uint8_t         state;
    uint8_t         pageAddr[6]; //The device to be connected back.

    uint32_t         devClass;
    TlkApiTimer_t  timer_recon;
    TlkApiTimer_t  timer_state_check;
} tlkmdi_btrecon_t;

tlkmdi_btrecon_t 是 BT 回连模块的上下文控制块,记录回连目标、重试策略与定时器三类关键信息:

tlkmdi_btrecon_t 字段说明

整个结构体随模块静态分配,生命周期与系统共存。应用层仅需在启动回连时填充 retry_num、pageAddr 与 devClass,其余字段由状态机内部维护,实现“一次配置、自动运转、超时自停、异常自恢复”的闭环回连流程。

回连状态机:

  • tlkmdi_btRecon_check_timer

​实时监控 ACL 链路状态,在 PAGE / WAIT_PAGE_CANCEL / WAIT_PROFILE / WAIT_STOP 四个关键态之间快速迁移;一旦检测到 page 成功、profile 就绪或用户终止,立即更新状态、重载定时器,保障回连流程单步闭环。

  • tlkmdi_btRecon_timer

​回连主控定时器按先10 s page 再 2 s page scan 节拍进行 retry 计数;超时则主动取消当前阶段并切换至下一状态,直至重试耗尽。任一步骤建立 ACL 或 retry_num 归零均自动停止定时器、清理控制块,并依据空闲链路数 fallback 到合适的模式,完成回连生命周期管理。

Reconnection State Machine

API 介绍

bool tlkmdi_btRecon_isInBusy(void);

该函数通过sTlkMdiBtReconCtrl.state判断回连是否处于 busy 状态;返回 true 表示处于回连状态,返回 false 表示处于非回连状态。

uint8_t tlkmdi_get_btRecon_state(void);

该函数通过sTlkMdiBtReconCtrl.state获取回连状态机的流转状态。

int tlkmdi_btRecon_start(uint8_t *pPageAddr, uint32_t devClass, uint8_t retry_num);

回连管理模块的关键函数,该接口直接由应用层调用。调用该函数会启动回连管理 Timer 和回连状态机,并且向 BT 控制器发送一条 HCI_Create_ConnectionHCI 指令。

Reconnection Start Flow

特别注意:该函数非线程安全,不允许在中断中调用。RTOS 环境下,如果在 audio、system 或 user 等其他非本线程任务中调用该接口,需要通过消息队列机制发送至 host 线程保障安全执行。

uint8_t *tlkmdi_btRecon_getPageAddr(void);

该函数用于获取当前正在回连设备的蓝牙地址,如果在非回连状态下调用该接口获取地址为 NULL。

int tlkmdi_btRecon_close(void);

该函数用于终止回连流程。调用后会取消 page 流程,同时停止状态机定时器和回连管理定时器,并将 tlkmdi_btrecon_t 控制块还原。

uint8_t tlkmdi_bt_recon_getRemindRetryNum(void);

该函数用于获取当前回连剩余的重试次数。

Inquiry 搜索

Inquiry 是传统蓝牙(BR/EDR)设备发现阶段的核心机制,作用是让查询发起设备(Inquirer) 主动扫描周围处于可发现模式(Discoverable Mode) 的蓝牙设备,并获取这些设备的基础信息(如设备地址、设备类型、时钟偏移等),为后续的配对与连接建立前提。

  • 设备发现:这是蓝牙设备建立连接的第一步,比如手机搜索蓝牙耳机、音箱、手环等外设的过程,本质就是发起 Inquiry 流程。
  • 信息收集:查询过程中,被发现的设备会向发起方广播自身的蓝牙设备地址(BD_ADDR)、设备类别(CoD, Class of Device)、时钟偏移等关键参数,帮助发起方识别设备类型并准备后续连接。

Inquiry 内部处理逻辑流程图:

Inquiry

SDK 中 Iquiry 的核心处理逻辑在 tlkmdi_btinq.c、tlkmdi_btinq.h 中,使用 TLK_MW_BTINQ_ENABLE 来管控,其核心控制块的结构类型如下:

typedef struct
{
    uint8_t state;      //当前inquiry的状态,refer to TLKMDI_BTINQ_STATE_ENUM,模块内部参数
    uint8_t busys;      //当前inquiry是否处于busy状态,模块内部参数
    uint8_t stage;      //当前inquiry的阶段,refer to TLKMDI_BTINQ_STAGE_ENUM,模块内部参数
    uint8_t inqType;    //用户传入的搜索设备的类型,refer to BTH_DEVICE_DTYPE_ENUM,模块内部会根据此参数来进行设备过滤上报

    uint8_t curNumb;    //当前正在处理的设备索引,模块内部参数
    uint8_t maxNumb;    //用户传入的最大搜索设备数量, SDK 内部会限制不能超过 TLKMDI_BTINQ_ITEM_NUMB数量
    uint8_t nameIdx;    //当前正在处理的设备名称索引,模块内部参数
    uint8_t rssiThd;    //用户传入的RSSI阈值,模块内部会根据此参数来进行设备过滤上报

    uint16_t inqWind;   //用户传入的Inquiry窗口
    uint16_t timeout;   //用户传入的Inquiry超时时间

    TlkApiTimer_t      timer; //定时器任务
    tlkmdi_btinq_item_t item[TLKMDI_BTINQ_ITEM_NUMB]; //设备信息缓存
} tlkmdi_btinq_ctrl_t;

typedef struct
{
    uint8_t rssi;     // 设备的RSSI
    uint8_t state;    // 设备的查询状态,表示设备名称是否获取完毕
    uint8_t smode;    // 设备的页扫描重复模式,refer to Page_Scan_Repetition_Mode_X
    uint8_t dtype;

    uint8_t nameLen;  // 此设备的名称长度
    uint8_t reserve;  // 保留字段
    uint8_t btaddr[6]; // 设备的蓝牙地址
    uint8_t btname[TLKMDI_BTINQ_NAME_LENS + 1]; // 设备的名称
    uint16_t reserve2B; // 预留字段
    uint16_t clkOff;  // 设备的时钟偏移
    uint32_t devClass;  // 设备的设备类别
} tlkmdi_btinq_item_t;

Inquiry 模块的初始化及相关 HOST 事件注册如下:

int tlkmdi_btinq_init(void)
{
    STATIC_ASSERT_THIS_FILE(IS_4BYTE_ALIGN(sizeof(tlkmdi_btinq_ctrl_t)));
    memset(&stlk_inq_ctrl, 0, sizeof(tlkmdi_btinq_ctrl_t));

    sTlkmdiBtInqReportCB   = NULL;
    sTlkmdiBtInqCompleteCB = NULL;

    tlksys_timer_createStatic(TLKSYS_TASKID_HOST, &stlk_inq_ctrl.timer, TLKMDI_BTINQ_TIMEOUT, false, tlkmdi_btinq_timer, NULL);

    return TLK_ENONE;
}

BTH_EVT_REGISTER (BTH_EVTID_INQUIRY_RESULT,     tlkmdi_btinq_resultEvt);
BTH_EVT_REGISTER (BTH_EVTID_INQUIRY_COMPLETE,   tlkmdi_btinq_completeEvt);
BTH_EVT_REGISTER (BTH_EVTID_GETNAME_COMPLETE,   tlkmdi_btinq_getNameCompleteEvt);

Inquiry 模块提供给用户调用的关键函数如下:

Inquiry 启动函数:

int tlkmdi_btinq_start(uint8_t inqType,
                       uint8_t rssiThd,
                       uint8_t maxNumb,
                       uint8_t inqWind)
{
    if (stlk_inq_ctrl.state != TLKMDI_BTINQ_STATE_IDLE) {
        return -TLK_EBUSY;
    }

    if (inqWind < 3) {
        inqWind = 3;
    } else if (inqWind > 60) {
        inqWind = 60;
    }

    if (inqWind > 100) {
        inqWind = 100;
    }
    if (maxNumb > TLKMDI_BTINQ_ITEM_NUMB) {
        maxNumb = TLKMDI_BTINQ_ITEM_NUMB;
    }

    if (maxNumb == 0) {
        maxNumb = TLKMDI_BTINQ_ITEM_NUMB;
    }

    stlk_inq_ctrl.inqType = inqType;
    stlk_inq_ctrl.curNumb = 0;
    stlk_inq_ctrl.nameIdx = 0;
    stlk_inq_ctrl.inqWind = ((uint32_t)inqWind * 1000) / TLKMDI_BTINQ_TIMEOUT_MS;
    stlk_inq_ctrl.maxNumb = maxNumb;
    stlk_inq_ctrl.rssiThd = rssiThd;

    stlk_inq_ctrl.state = TLKMDI_BTINQ_STATE_INQUIRY;
    stlk_inq_ctrl.stage = TLKMDI_BTINQ_INQUIRY_STAGE_START;

    tlkapi_trace(TLKMDI_BTINQ_DBG_FLAG, TLKMDI_BTINQ_DBG_SIGN, "tlkmdi_btinq_start type[%d]...", stlk_inq_ctrl.inqType);

    tlksys_timer_reStart(TLKSYS_TASKID_HOST, &stlk_inq_ctrl.timer);

    return TLK_ENONE;
}

Inquiry 结束函数:

void tlkmdi_btinq_close(void)
{
    uint8_t stage;

    if (stlk_inq_ctrl.state == TLKMDI_BTINQ_STATE_IDLE) {
        return;
    }
    if (stlk_inq_ctrl.state == TLKMDI_BTINQ_STATE_CLOSING) {
        return;
    }

    stage = TLKMDI_BTINQ_CLOSING_STAGE_INQUIRY_OVER;
    if (stlk_inq_ctrl.state == TLKMDI_BTINQ_STATE_INQUIRY) {
        if (stlk_inq_ctrl.stage != TLKMDI_BTINQ_INQUIRY_STAGE_WAIT_CANCEL) {
            stage = TLKMDI_BTINQ_CLOSING_STAGE_CANCEL_INQUIRY;
        } else {
            stage = TLKMDI_BTINQ_CLOSING_STAGE_INQUIRY_OVER;
        }
    }

    stlk_inq_ctrl.state = TLKMDI_BTINQ_STATE_CLOSING;
    stlk_inq_ctrl.stage = stage;

    stlk_inq_ctrl.timeout = TLKMDI_BTINQ_WAIT_CANCEL_TIMEOUT;
}

设备上报接口的注册:

void tlkmdi_btinq_regCallback(TlkMdiBtInqReportCallBack reportCB, TlkMdiBtInqCompleteCallBack completeCB)
{
    sTlkmdiBtInqReportCB   = reportCB;  // 设备上报接口注册
    sTlkmdiBtInqCompleteCB = completeCB;  // 设备完成接口注册
}

服务查询

SDP (Service Discovery Protocol,服务发现协议) 是传统蓝牙 (BR/EDR) 架构的核心协议之一,也是蓝牙设备完成「设备发现」后,定位并获取对方可用服务信息 的关键流程。其核心目标是让蓝牙设备(如手机)发现另一台设备(如蓝牙耳机)支持的服务类型(如 A2DP 音频传输、HFP 通话、HID 键鼠控制等)、服务对应的协议参数(如 L2CAP 通道 ID、RFCOMM 端口号),为后续建立业务连接提供核心依据。

SDP 服务器(Server):提供服务的设备(如蓝牙耳机),内置「服务记录数据库」,存储自身所有服务的描述信息;

SDP 客户端(Client):发起查询的设备(如手机),向服务器发送查询请求,解析返回的服务信息。

SDP 查询使用基于 L2CAP 协议的固定通道(PSM=0x0001)信道进行通信。客户端和服务器在建立任何正式的服务连接之前,就可以通过这个信道进行查询。SDP ,整个查询流程分为 4 个步骤:

(1) 建立 SDP 会话:客户端与服务器建立蓝牙基础连接(L2CAP 通道)后,默认通过 PSM=0x0001 的固定通道发起 SDP 请求,无需额外协商。

(2) 客户端发送查询请求:客户端可发起两种核心查询类型:

  • 按服务类 UUID 查询:指定目标服务的 UUID(如 0x110B),查询服务器是否支持该服务及对应参数;

  • 获取服务器所有服务的概要信息(如服务名称、类型),用于遍历设备所有可用服务。

(3) 服务器响应查询结果:服务器解析请求后,从本地服务记录数据库中匹配符合条件的服务,将服务记录的指定属性打包成「SDP 响应数据包」返回给客户端。

(4) 客户端解析与连接建立:客户端解析响应中的服务参数(如 RFCOMM 通道号),基于该参数发起具体服务的连接(如通过 RFCOMM 端口建立 A2DP 音频通道),SDP 会话完成后可关闭(或保留用于后续重查)。

SDK 目前会将查询到的对端设备支持的服务 channel 与设备绑定,并作为配对信息写入 flash 中。在设备连接时,仅当未读取到此设备的有效服务 channel 信息时,SDK 会主动触发 SDP 查询,具体触发的函数如下:

int btp_sdpclt_connect(uint16_t aclHandle);

对端设备支持的服务 channel 信息上报接口如下:

BTP_EVT_REGISTER (BTP_EVTID_PROFILE_CHANNEL,         tlkmdi_btacl_profileChannelEvt);
static int tlkmdi_btacl_profileChannelEvt(uint8_t *pData, uint16_t dataLen)
{
    (void)dataLen;
    btp_channelEvt_t    *pEvt;
    tlkmdi_btacl_item_t *pItem;

    pEvt  = (btp_channelEvt_t *)pData;
    pItem = tlkmdi_btacl_getUsedItem(pEvt->handle);
    if (pItem == NULL) {
        tlkapi_error(TLKMDI_BTACL_DBG_FLAG, TLKMDI_BTACL_DBG_SIGN, "tlkmdi_btacl_profileChannelEvt: error - no node");
        return TLK_ENONE;
    }

    if (pEvt->service == BTP_SDP_SRVCLASS_ID_HANDSFREE) {
        pItem->hfChannel = pEvt->channel;
    } else if (pEvt->service == BTP_SDP_SRVCLASS_ID_HANDSFREE_AGW) {
        pItem->agChannel = pEvt->channel;
        if (pEvt->channel != 0) {
            if (pItem->active == false) { 
                tlkmdi_tinySql_setPairingDeviceRfcChid(pItem->btaddr, pEvt->channel, TLKMDI_BT_RFC_CHID_HFP);   
            } 
            btp_tws_set_rfcommChnID(pEvt->channel, true); 
        }  
    } else if (pEvt->service == BTP_SDP_SRVCLASS_ID_SERIAL_PORT) {
        pItem->sppChannel = pEvt->channel;
        if (pEvt->channel != 0) {
            btp_tws_set_rfcommChnID(pEvt->channel, false);  
        } 
        tlkmdi_tinySql_setPairingDeviceRfcChid(pItem->btaddr, pEvt->channel, TLKMDI_BT_RFC_CHID_SPP);   
    } else if (pEvt->service == BTP_SDP_SRVCLASS_ID_IAP2_TEMP) {
        pItem->iapChannel = pEvt->channel;
        tlkmdi_tinySql_setPairingDeviceRfcChid(pItem->btaddr, pEvt->channel, TLKMDI_BT_RFC_CHID_IAP); 
    } else if (pEvt->service == BTP_SDP_SRVCLASS_ID_PBAP_PSE) {
        pItem->pbapChannel = pEvt->channel;
        tlkmdi_tinySql_setPairingDeviceRfcChid(pItem->btaddr, pEvt->channel, TLKMDI_BT_RFC_CHID_PBAP); 
    } else if (pEvt->service == BTP_SDP_SRVCLASS_ID_IMAGING_RESPONDER) {
        pItem->bipChannel = pEvt->channel;
        tlkmdi_tinySql_setPairingDeviceRfcChid(pItem->btaddr, pEvt->channel, TLKMDI_BT_RFC_CHID_BIP); 
    }
    return TLK_ENONE;
}

协议连接

蓝牙 Profile(协议子集)是经典蓝牙(BR/EDR)设备间实现特定功能(如通话、音乐播放)的核心协议规范,它基于底层的 L2CAP、SDP 等协议,定义了设备角色、数据交互流程、编码格式等细节。对于消费电子(如蓝牙耳机、音箱)开发而言,Profile 连接的本质是 “设备角色协商->SDP 服务发现逻辑链路建立->数据传输” 的完整流程,不同 Profile 对应不同的功能场景。

常用的 Profile 有:

  • SDP(Service Discovery Protocol): 用于服务发现,确定设备所支持的 Profile。
  • A2DP(Advanced Audio Distribution Profile): 用于高质量音频流的传输,通常用于将音频从手机或电脑传输到蓝牙耳机或音响。
  • AVRCP(Audio/Video Remote Control Profile): 允许用户控制音频和视频设备的播放,例如音量调节、播放/暂停等。
  • HFP(Hands-Free Profile): 允许免提设备与手机进行通信,常用于车载免提系统。
  • HID(Human Interface Device): 人机接口设备,如鼠标、键盘、手柄、游戏控制器等。
  • SPP(Serial Port Profile): 串行端口协议是一种特殊的 Profile,它是一种无连接的 Profile,用于实现串行端口通信。

在使用相关的 profile 时,首先需要打开 profile 的功能,然后才能进行相关的操作。

//BT Stack Configuration//
#define TLK_STK_BT_ENABLE         1
#define TLKBTP_CFG_RFC_ENABLE     (1 && TLK_STK_BT_ENABLE)
#define TLKBTP_CFG_SPP_ENABLE     (1 && TLKBTP_CFG_RFC_ENABLE)
#define TLKBTP_CFG_HFP_ENABLE     (1 && TLKBTP_CFG_RFC_ENABLE)
#define TLKBTP_CFG_HFPHF_ENABLE   (1 && TLKBTP_CFG_HFP_ENABLE)
#define TLKBTP_CFG_A2DP_ENABLE    (1 && TLK_STK_BT_ENABLE)
#define TLKBTP_CFG_A2DPSNK_ENABLE (1 && TLKBTP_CFG_A2DP_ENABLE)

通用连接流程:

  • 发现与配对 : inqiury/page 发现设备,完成配对、认证与加密,生成并保存 Link_Key,建立 ACL 链路。
  • SDP 查询 : Client 向 Server 发起 SDP 查询,确认其是否支持目标 Profile(如 A2DP Sink),获取 PSM、服务 UUID、通道信息等关键参数。
  • L2CAP 通道建立 : Client 基于 PSM 发起 L2CAP 连接请求,协商 MTU 与流控参数;Server 被动响应并分配 CID,通道就绪后进入数据传输准备。
  • Profile 特定协商 : 按 Profile 规范完成能力协商,如 A2DP 协商编码格式(SBC/AAC/LDAC);HFP 协商 AT 指令集与 SCO/eSCO 链路参数。
  • 数据传输与控制 :A2DP 通过 L2CAP 传输音频流;HFP 用 SCO/eSCO 传输语音,同时通过 L2CAP 发送 AT 指令;AVRCP 发送控制指令并接收状态反馈。
  • 断开连接 :可由 Client 主动断开 L2CAP 通道,或 Server 主动拒绝/关闭;断开后释放 CID、PSM 与相关资源。

蓝牙 Profile 都定义了客户端(Client)和 服务器端(Server)的角色,Client 角色设备发起 Profile 功能请求,Server 角色设备响应请求并提供服务。

在 SDK 中应用中,在蓝牙加密流程完成后,会进行 Profile 的连接。首先会根据当前连接设备的地址去查找是否与该设备有连接记录,如果有记录,则会直接连接,此时会调用

void app_btmgr_appendProfile(uint16 aclHandle);

该函数将需要连接的 Profile 加入到连接管理器的队列中依次去创建连接,默认会去创建RFCOMMHFPA2DPAVRCP等 Profile。

如果没有记录,则会调用btp_sdpclt_connect去查询对端设备支持的 Profile:

int btp_sdpclt_connect(uint16 aclHandle);

该函数会以 Client 的角色去创建 SDP 连接。

SDK 中判断流程如下:

SDK Profile

为了可以获取 profile 的连接状态,需要注册 profile 状态的事件回调函数:

BTP_EVT_REGISTER (BTP_EVTID_PROFILE_CONNECT,         tlkmdi_btacl_profileConnectEvt);
tlkmdi_btacl_regProfileConnectCB(app_btmgr_ProfConnCB);

该函数会在 profile 连接成功或失败时被调用。

在 profile 断开时,会有相应的事件通知,应用层需要注册相应的事件回调函数:

BTP_EVT_REGISTER (BTP_EVTID_PROFILE_DISCONN,         tlkmdi_btacl_profileDisconnEvt);
tlkmdi_btacl_regProfileDisconnCB(app_btmgr_ProfDiscCB);

profile 连接:

Profile

  • L2CAP 连接

所有 host 协议连接都基于 L2CAP 连接,Setup 有 4 个流程:

  • Connection_Request\Connection_Response
  • Configure_Request\Configure_Response
  • Configure_Request\Configure_Response
  • Configure_Request\Configure_Response

它们的发起方是不一样的。第一次由Central发起,第二次由Peripheral发起。最终会 产生一个Source CID和一个Destination CID用于识别当前的通路。

  • SDP 服务查询

SDP 服务查询获取对端设备支持什么应用协议,在 SDP 服务查询之前先建立 L2CAP 链路,然后发查询命令给对方,等待结果并解析上传,查询结束后释放 L2CAP 链路;查询结束后知道对端支持的服务类型后,根据需要创建连接,比如手机知道耳机支持 HFP、A2DP、AVRCP 等协议。

  • HFP 连接

HFP 基于 RFCOMM 连接之上,手机的音频的连接 AG 和 HF 侧都可以发起,连接过程中的消息交互及流程大体相同,HFP 连接过程如下图所示。

HFP Connection

涉及到的 AT 指令交互如下:

HFP AT Instruction

指令详细含义请参考HFP

  • A2DP 连接

A2DP 基于 AVDTP 连接之上,在 A2DP 连接的过程中,会创建两种 channel,分别是Signaling ChannelMedia Transport Channel,前者用于命令的发送,后者用于音频数据的传输。在Signaling Channel通路建立后,就会进行一些参数的协商,比如编码格式、采样率、数据传输通道数、数据传输通道的优先级等。一般的参数协商流程如下图所示。

A2DP Connection

通话管理

在 SDK 中,设备既可以连接耳机也可以连接手机,就连接 HFPprofile 而言,在连接耳机时设备为 AG 角色,在连接手机时设备作为 HF 角色。

(1) 通话控制

通话控制分为通话的接听、挂断、拒接等功能。来电的接听、挂断、拒接来电等功能可以通过以下接口控制:

int tlkapp_audio_callCtrl(uint8_t opcode)
{
    bool res = false;
    if(opcode != TLKAUD_OPCODE_CALL_ACCEPT && opcode != TLKAUD_OPCODE_CALL_HUNGUP){
        return -TLK_EPARAM;
    }

    const tlkapp_audioScheduler_node_t *nowTask = tlkapp_audioScheduler_getRunningTask();
    if (nowTask != nullptr) {
        res = tlkapp_audio_modinfOperate((uint16)nowTask->taskId, (uint16)nowTask->info.optype, &opcode, 1);
    }
    if (res == false) {
        uint16 msgID = opcode == TLKAUD_OPCODE_CALL_ACCEPT ? TLKSYS_BT_MSGID_HF_SEND_CALL_ACCEPT : TLKSYS_BT_MSGID_HF_SEND_CALL_HUNGUP;
        uint16 handle = 0xFFFF;
        return tlksys_sendMsg(TLKSYS_TASKID_HOST, msgID, &handle, 2);      
    }
    return TLK_ENONE;
}

在通话状态发生变化时,AG会通过+CIEV通知当前的状态变化,HF在收到+CIEV指令时,HF端会调用btp_hfphf_recvCievCmdHandler()函数进行数据解析主要涉及到callcallsetup两个状态的变化。

call:标准呼叫状态指示器,其中:

  • <value>=0表示没有正在进行的呼叫
  • <value>=1表示至少有一个调用正在进行中

callsetup:呼叫建立状态指示器,其中:

  • <value>=0表示当前未处于呼叫设置中
  • <value>=1表示正在进行来电处理
  • <value>=2表示正在进行拨出呼叫设置
  • <value>=3表示远程方在拨出呼叫中收到警报。

接听电话:

在接听电话时,会检查当前的通话状态,检查callcallsetup的状态,如果call=1且callsetup=1时,则会调用btp_hfphf_answer发送"ATA\r指令回复AG。

HFP Call

拒接/挂断电话:

在拒接或者挂断电话时,会检查当前的通话状态,检查callcallsetup的状态,如果call=1且callsetup=1时,则会调用btp_hfphf_reject发送"AT+CHUP指令回复AG。

HFP Hangup Call

/* 发送对应的事件消息 */
tlksys_sendMsg(TLKSYS_TASKID_HOST, TLKSYS_BT_MSGID_HF_SEND_CALL_ACCEPT, &handle, sizeof(handle));
/* 接听 */
tlkapp_audio_callCtrl(TLKAUD_OPCODE_CALL_ACCEPT);
/* 发送对应的事件消息 */
tlksys_sendMsg(TLKSYS_TASKID_HOST, TLKSYS_BT_MSGID_HF_SEND_CALL_HUNGUP, &handle, sizeof(handle));
/* 拒接 */
tlkapp_audio_callCtrl(TLKAUD_OPCODE_CALL_HUNGUP);

(2) 通话音量控制

通话音量控制分为音量的设置和音量的变化通知。

音量设置:

音量设置一般指的是设备端去设置音量,手机端接收到设置音量的指令后,根据设置的音量值来调整音量。在按键事件触发后,tlkapp_btmgr_setHfpVolumeDeal()函数会被调用,该函数会将要设置的音量等级发送给手机端(AT+VGS命令)。

int tlkapp_btmgr_setHfpVolumeDeal(uint8_t *pData, uint8_t dataLen);
int btp_hfphf_setSpkVolume(uint08 spkVolume);

HFP AT VGS

音量通知:

当手机端调整音量时,音量的变化会同步到耳机端,在手机端音量发生变化时,设备端会收到+VGS消息,设备端会将音量的变化通知给应用层,在codec工作时实时生效。

int btp_send_hfphfVolumeChangedEvt(uint16 aclHandle, uint08 type, uint08 volume)
{
    btp_hfpVolumeChangedEvt_t evt;
    evt.handle  = aclHandle;
    evt.volume  = volume;
    evt.volType = type;
    return btp_send_event(BTP_EVTID_HFPHF_VOLUME_CHANGED, (uint08 *)&evt, sizeof(btp_hfpVolumeChangedEvt_t));
}

为了及时响应音量的变化,应用层需要注册音量变化的事件回调函数:

BTP_EVT_REGISTER (BTP_EVTID_HFPHF_VOLUME_CHANGED,        tlkmdi_hfphf_volumeChangedEvt);

tlkmdi_hfphf_volumeChangedEvt()函数会在收到音量变化的消息时被调用,然后通过事件的形式将音量变化通知给应用层去做相应的处理。

HFP VGS

(3) 通话触发

通话的触发HF和AG都可以作为发起方,但Codec Connection Setup都是由AG发起的,HF作为被连接方。

  • 在作为AG触发通话时,可以调用以下接口:
/******************************************************************************
 * Function: tlkmdi_bthfpag_createSco
 * Descript: Create a one-way SCO connection via AG.
 * Params:
 *        @pBtAddr[IN]--The device address.
 * Return: Returning TLK_ENONE(0x00) means the send process success.
 *         If others value is returned means the send process fail.
 *******************************************************************************/
int tlkmdi_bthfpag_createSco(uint08 *pBtAddr);

由于需要传入连接的设备的地址,所以需要先获取到设备的地址。连接设备的地址可以通过以下接口获取:

uint08 *bth_handle_getBtAddr(uint16 aclHandle);
uint16 btp_hfp_getAgHandle(void);

获取到设备的地址后,就可以调用tlkmdi_bthfpag_createSco()接口创建 SCO 连接。

Audio Connection Setup by AG

  • 在作为 HF 角色触发通话时,可以调用以下接口:
/******************************************************************************
 * Function: btp_hfphf_codecConn
 * Descript: Used by the HF to request the AG to start the codec connection procedure.
 * Params:
 *        @aclHandle[IN]--The acl handle.
 * Return: Returning TLK_ENONE(0x00) means the send process success.
 *         If others value is returned means the send process fail.
 *******************************************************************************/
int btp_hfphf_codecConn(uint16_t aclHandle);

HF 角色触发通话实际上是发送AT+BCC指令到 AG,AG 在接收到该指令后会回复OK,然后发起Codec Connection Setup相关的指令。

Audio Connection Setup by HF

(4) Siri

Siri 是苹果公司推出的智能语音助手,主要集成于 iOS、iPadOS、macOS、watchOS 等苹果生态系统中,核心功能是通过自然语言交互帮助用户完成各类操作,同时在嵌入式场景中也与硬件(如 iPhone 麦克风、蓝牙设备)深度联动。

在用户层 siri 的触发和关闭都是通过以下接口来实现的:

int tlkmdi_bthfphf_assistant(uint16 handle);

函数的执行流程为tlkmdi_bthfphf_assistant()->btp_hfphf_siri_ctrl()->btp_hfphf_sendIphoneSiriCtrlProc()。本质上 siri 的触发与关闭就是一组AT指令的交互(AT+BVRA=1表示触发 Siri,AT+BVRA=0表示关闭 Siri)。

音乐管理

(1) 音量管控

音量控制分为音量的设置和音量的变化通知。

音量通知:

为了知道手机音量的变化,需要注册音量变化的事件回调函数。

BTP_EVT_REGISTER (BTP_EVTID_AVRCP_VOLUME_CHANGED,           tlkmdi_btavrcp_volumeChangeEvt);

tlkmdi_btavrcp_volumeChangeEvt会在手机音量发生变化时被调用,并将变化的音量值和连接的 handle 参数传入该函数。然后通过事件的形式将音量变化通知给应用层:

tlksys_sendMsg(TLKSYS_TASKID_AUDIO, TLKSYS_AUD_MSGID_HOST_EVT_COME, buffer, bufferLen);

在收到TLKSYS_AUD_MSGID_HOST_EVT_COME事件后,应用层会调用以下函数来处理对应的事件:

int tlkmdi_audio_hostif_getHostEvtDeal(uint8_t *pData, uint8_t dataLen)
void tlkmdi_audio_btif_getHostEvtDeal(uint8_t *pData, uint8_t dataLen)

tlkmdi_audio_btif_getHostEvtDeal函数中,根据事件类型,调用相应的处理函数。

void tlkmdi_audio_btif_getHostEvtDeal(uint8_t *pData, uint8_t dataLen)
{
    if(dataLen < sizeof(tlksys_msg_hostEvt_t)){
        return;
    }
    tlksys_msg_hostEvt_t * evt = (tlksys_msg_hostEvt_t *)pData;
    if(evt->hostType != TLKSYS_MSG_HOST_TYPE_BT){
        return;
    }
    switch(evt->msgID){
        case TLKSYS_MSG_BT_HOST_EVT_TYPE_VOLUME_CHG:{
            if(evt->dataLen != sizeof(tlksys_msg_hostEvt_btVolChg_t)){
                return;
            }
            tlkmdi_audio_btif_getVolumeChgDeal((tlksys_msg_hostEvt_btVolChg_t *) evt->data);
        }break;
        case TLKSYS_MSG_BT_HOST_EVT_TYPE_AUD_STATE_CHG:{
            if(evt->dataLen != sizeof(tlksys_msg_hostEvt_btAudStateChg_t)){
                return;
            }
            tlkmdi_audio_btif_getAudStateChgDeal((tlksys_msg_hostEvt_btAudStateChg_t *) evt->data);
        }break;
    }
}
typedef enum
{
    TLKSYS_MSG_BT_HOST_EVT_TYPE_VOLUME_CHG  = 0x00,
    TLKSYS_MSG_BT_HOST_EVT_TYPE_AUD_STATE_CHG,
} TLKSYS_MSG_BT_HOST_EVT_TYPE_ENUM;
字段 说明
TLKSYS_MSG_BT_HOST_EVT_TYPE_VOLUME_CHG 手机音量发生改变时向上层触发该事件
TLKSYS_MSG_BT_HOST_EVT_TYPE_AUD_STATE_CHG 音乐播放状态发生改变时向上层触发该事件

上层需要注册音量变化的事件回调函数:

void tlkmdi_audio_btif_regMusicVolChgCB(TlkMdiAudBtifVolChgCB cb)

音量设置:

音量控制一般指的是设备端去设置音量,手机端接收到设置音量的指令后,根据设置的音量值来调整音量。

设备端控制音量的调整是通过按键触发的,需要在按键事件设置中配置调整音量增减的事件。在 SDK 中,音量增减的配置在sApp_key_default_config中设置。最终会调用 Audio 任务接收 message 的函数int tlkapp_audio_msgHandle()进行事件的处理。

int tlkapp_audio_msgHandle(uint8_t msgID, uint8_t *pData, uint16 dataLen);
int tlkapp_audio_volumeCtrl(uint8_t isInc);

tlkapp_audio_volumeCtrl()函数会根据 isInc 参数来判断是增加还是减少音量,然后调用对应 Audio 任务的接口来设置音量。最终会通过 BTP 协议将音量设置指令发送给手机端。

tlksys_sendMsg(TLKSYS_TASKID_HOST, TLKSYS_BT_MSGID_SET_AVRCP_VOLUME, buffer, buffLen);
int tlkapp_btmgr_setAvrcpVolumeDeal(uint08 *pData, uint08 dataLen);

音量控制的总体流程如下:

Music Volume

(2) 播放状态管控

为了知道手机端的音乐播放状态,需要注册音乐播放状态的事件回调函数。

BTP_EVT_REGISTER (BTP_EVTID_A2DPSNK_STATUS_CHANGED,             tlkmdi_bta2dp_statusChgCB);
BTP_EVT_REGISTER (BTP_EVTID_AVRCP_STATUS_CHANGED,               tlkmdi_btavrcp_statusChgCB);
  • tlkmdi_bta2dp_statusChgCB()函数会在A2DP播放状态改变时调用。

  • tlkmdi_btavrcp_statusChgCB()函数会在AVRCP播放状态改变时调用。

当手机端的音乐播放状态发生变化时,会调用相应的事件回调函数,然后调用tlkmdi_bta2dp_sendHostMusicStateChgEvt()通知到应用层。

音乐在不同时期的状态变化如下所示:

触发事件 A2DP 状态变化 AVRCP 状态变化
手机发起“播放”指令 IDLE -> STREAMING STOPPED/PAUSED -> PLAYING
手机发起“暂停”指令 STREAMING -> SUSPENDED PLAYING -> PAUSED
耳机主动“切歌” 保持 STREAMING(新音频流) PLAYING -> PLAYING(新曲目)
蓝牙断开连接 任意状态 -> DISCONNECTED 任意状态 -> DISCONNECTED
音频播放完毕 STREAMING -> IDLE PLAYING -> STOPPED

应用层通过执行以下接口控制codec的相关动作:

void tlkmdi_bta2dp_sendHostMusicStateChgEvt(uint16 handle,uint08 state);
void tlkmdi_audio_btif_getHostEvtDeal(uint8_t *pData, uint8_t dataLen);
static void tlkmdi_audio_btif_getAudStateChgDeal(tlksys_msg_hostEvt_btAudStateChg_t *evt)

以开始播放音乐为例,在作为SNK时,在收到SRC的控制指令后,SNK的响应流程如下所示,暂停、上下曲的指令响应与开始播放的指令响应一致,只是SNK的响应数据不同。

Music A2DP

不管是音量管控还是状态管控,都会调用以下函数去处理,该函数会根据当前的业务场景将对应的事件分发到对应的处理函数:

bool tlkapp_audio_modinfOperate(uint16 handle, TLKAUD_TYPE_ENUM optype, uint8_t *pData, uint16 dataLen)

(3) 多媒体UI

在音乐场景中,应用层的UI涉及到音乐的播放、暂停、下一首、上一首等。通常使用下面几个接口来实现:

int tlkapp_audio_PlayPause(void);
bool tlkapp_audio_playNext(void);
bool tlkapp_audio_playPrev(void);
int tlkapp_audio_volumeCtrl(uint8_t isInc);

在控制多媒体时,实际上就是向对端设备发送指令,对端设备解析指令并执行相应的操作,这些指令的发送依赖于特定的协议。

以SNK端触发播放音乐为例,从按键触发到相应指令发送出去的流程如下:

Music Control

作为SNK端,SNK端触发播放、暂停、下一首、上一首的流程是相同的,只不过发送的指令不同,以下是要通过AVRCP协议发送的指令:

AUD_BTIF_AVRCP_KEYID_PLAY                = 0x44,
AUD_BTIF_AVRCP_KEYID_STOP                = 0x45,
AUD_BTIF_AVRCP_KEYID_PAUSE               = 0x46,
AUD_BTIF_AVRCP_KEYID_RECORD              = 0x47,
AUD_BTIF_AVRCP_KEYID_REWIND              = 0x48,

BLE 相关

BLE Host 架构

BLE Host 即蓝牙低功耗主机,是蓝牙低功耗(Bluetooth Low Energy)协议栈的上层核心组件,在整个蓝牙通信架构中承担着协议逻辑管控、应用交互衔接与数据调度管理的关键作用,向下对接 BLE 控制器(Controller),向上为各类应用程序提供标准化的通信接口,是保障蓝牙低功耗设备间稳定、高效通信的核心中枢。

从具体功能来看,BLE Host 的核心能力主要体现在以下几个方面:

链路层与连接管理:负责蓝牙设备的配对、绑定与连接建立,包括设备发现、连接参数协商、连接维护与断开等流程,能够根据应用需求调整连接间隔、超时时间等关键参数,在保证通信稳定性的同时优化功耗表现。

协议层规范执行:承载并运行蓝牙低功耗协议栈的上层协议,如通用属性协议(GATT)、通用访问协议(GAP)、安全管理协议(SM)等。其中,GATT 定义了数据传输的属性结构与交互方式,让设备间能清晰识别并读写特征数据;GAP 则规范了设备的角色行为,如广播者、观察者、主设备、从设备的模式切换;SM 负责配对过程中的加密与认证,保障通信数据的安全性。

数据交互与业务适配:作为应用程序与底层硬件的中间桥梁,BLE Host 接收应用层下发的指令与数据,将其封装为符合蓝牙协议规范的数据包,再通过控制器发送至对端设备;同时,它会解析从控制器接收的对端数据包,提取有效信息并向上传递给应用层,实现业务数据的双向流转。

多设备与多链路协调:支持同时管理多条蓝牙连接链路,协调不同设备间的通信资源分配,避免数据传输冲突,尤其适用于网关类设备同时连接多个从设备的场景,保障多设备通信的有序性。

该SDK中,BLE Application场景非常多,传统的BLE Device设备、BLE HID Host、BLE Audio等都需要基于Host架构开发,会有单核芯片、多核芯片需要适配。为了简化后面的开发逻辑,对于Host重新进行架构设计是非常有必要的。

新的BLE Host架构如下图所示,按照代码层级分分为三层分别是软件适配层(Software Abstraction Layer, SAL),HCI接口层(HCI Interface Layer, HIL),协议栈层(Protocol Stack Layer, PSL)。

BLE Host层级

按照功能模块划分,BLE Host分为以下几个模块:

Software Abstraction Layer(SAL): 软件抽象层,主要负责与平台的交互,基础组件的抽象,比如日志打印,NVRAM读写等。

Host Controller Interface(HCI): HCI接口,主要负责与HCI协议栈的交互,包括HCI命令的发送、事件的处理、ACL Data的接收、ACL Data的发送,ISO Data的接收、ISO Data的发送等。

Generic Access Profile(GAP): 通用访问协议层,主要负责对HCI命令的封装,包括设备发现、连接参数协商、连接建立、连接断开等。

Logical Link Control and Adaptation Protocol(L2CAP): L2CAP协议层,主要负责管理连接的逻辑通道,包括通道建立、通道释放、通道数据传输等。

Generic Attribute Profile(GATT): 通用属性协议层,主要负责管理设备的服务、特征、描述符等,包括服务发现、特征发现、特征读写等。

Services: 服务层,主要负责实现GATT服务,包括通用服务、自定义服务等。

Profiles: Profile层,主要负责实现GATT Profile,包括通用Profile、自定义Profile等。

ISO data: ISO数据层,主要负责实现ISO数据协议的数据流控,分发等。

Host Management: 主机管理层,主要负责管理Host的通用信息,比如controller的芯片信息、版本信息、系统信息,ACL连接的数量管理等功能。

BLE Host架构

BLE GAP

在该BLE Host设计中,GAP模块负责对于HCI命令的封装,HCI命令流的整理,HCI事件的处理,以及GAP事件的分发的作用。

GAP SDK 目前已经实现的模块有:

  • GAP BLE ACL
  • GAP BLE Advertising
  • GAP Filter
  • GAP BLE ISO
  • GAP Host Address
  • GAP Periodic Advertising
  • GAP BLE Scan
  • GAP Event Dispatch

下面会依次介绍这些模块的功能,以及如何使用。

GAP BLE Advertising module

BLE的广播模块分为legacy advertising和extended advertising两种,legacy advertising是最基本的广播模式,extended advertising可以携带更多的广播信息,支持多路广播同时存在。具体的使用方式可以参考ble example 的BLE ADV demo。

GAP BLE ACL module

BLE的连接模块,包括BLE Central中创建连接;BLE Peripheral使能Telink Controller的Latency,BLE Peripheral回连等功能;BLE ACL中的Connection parameter update,Feature exchange,Data Length Update,PHY Update,断连等功能。具体的可以参考GAP ACL模块的API接口注释。

GAP Filter module

BLE的过滤模块,目前仅支持BLE Filter Accept List功能,可以设置不同的过滤条件,广播阶段或者建立连接阶段过滤掉不符合条件的设备。

GAP BLE ISO module

BLE的ISO模块,ISO是Core 5.2后新增的功能,目前支持BIG和CIS两大模块,包括BIG Broadcast,BIG Synchronous,CIS Central和CIS Peripheral。通常用户不会直接调用。

GAP Host Address module

BLE的主机地址,现在支持Resolvable private address,Static device address和Non-resolvable private address。详细的使用方式参考BLE example的random address demo。

GAP Periodic Advertising module

BLE的周期性广播模块,包括周期性广播的同步和广播模式,在SDK中通常和BIG相关。

GAP BLE Scan module

BLE的扫描模块,包括BLE扫描的启动、停止、过滤等功能,对于扩展广播和传统广播的事件处理做了同一处理,方便应用层的调用。具体的使用可以参考BLE Example的BLE Scan demo。

GAP Event Dispatch module

BLE的事件分发模块,包括GAP事件的分发,包括GAP事件的处理,包括GAP事件的回调处理。通过注册的方式,实现事件消息的订阅和获取,解耦不同模块的耦合。

BLE ATT

BLE GATT基本单位Attribute:

GATT定义了两种角色:Server和Client。Server通常包含多组service,client通过ATT层的指令操作service。一组service是由一个service UUID与多个characteristic UUID组成,每个UUID会有多条Attribute构成,每条Attribute都具有⼀定的信息量,用来描述UUID的信息。

注意

  • ACL的两种角色central和peripheral,与GATT层的server和client解耦。ACL Peripheral角色既可以是GATT server也可以是GATT client,ACL Central角色同理。
  • 后续介绍到的所有API,ACL连接句柄,通常不区分ACL角色。除非profile中有明确规定该profile只能在某个ACL角色下使用。

GATT server的结构

⼀条 Attribute 包含Attitude handle、Attribute Type、Attribute value、Attribute table部分。

1) Attribute Type: UUID

UUID 用来区分每⼀个 attribute 的类型,其全⻓为16个bytes。 BLE 标准协议中UUID⻓度定义为2个bytes,这是因为所有设备都遵循同⼀套转换⽅法,将2个bytes的UUID转换成16 bytes。

Server使用蓝⽛标准协议中的2 bytes的UUID时,Client都会把它转换为16 bytes的UUID匹配。

几乎所有的标准16bits UUID定义在stack/ble/host_v1/att/inc/uuid16bit.h

Telink 私有的⼀些 profile(OTA、 MIC、Speaker等),标准蓝牙里面不支持,在 stack/ble/host_v1/att/inc/uuid128bit.h 中定义这些私有的UUID⻓度为16 bytes。

2) Attribute Handle

Server拥有多个Attribute,这些Attribute组成⼀个Attribute Table。在 Attribute Table中,每⼀个Attribute都有⼀个唯一Attribute Handle值,用来区分Attribute。server和client建⽴连接后,client通过Service Discovery过程解析读取到server的 Attribute Table,并根据Attribute Handle的值来对应每⼀个不同的Attribute,这样它们后⾯的数据通信只要带上Attribute Handle,对⽅就知道是哪个Attribute的数据了。

Attribute Handle的取值范围为0x0001~0xFFFF。

3) Attribute Value

每个 Attribute 都有对应的 Attribute Value,用来作为 request、 response、 notification 和 indication 的数据。在该BLE SDK中, Attribute Value 用指针和指针所指区域的⻓度来描述。

4) Attribute table

一些常用的attribute table,该SDK已经帮忙实现了,实例文件在stack/ble/host_v1/services中。开发者可用直接使用,或者修改后使用。

Attribute Table and Service Group:

为了适应复杂LE应用(例如LE Audio) 开发的多GATT service的特点,SDK设计了attribute table和service group结合的方式。

Attribute table由多个基本的Attribute组成,attribute table通常只包含一个service UUID和多条characteristic UUID。这不是强制的要求,只是建议

Service group包含一个完整的attribute table,起始句柄,结束句柄,读attribute回调,写attribute回调,指向新service group指针。

Attribute 基本定义为:

struct atts_attribute {
    uint8_t perm;           // refer to ATT_PERMISSIONS_BITMAPS.
    uint8_t uuidLen;        // UUID length, usually 2 or 16, 4 maybe used.
    const uint8_t *uuid;    // UUID value, 16 or 128 bits.
    uint16_t *attrValueLen; // attribute value length, point to the actual length of attribute value. 
    uint16_t maxAttrLen;    // maximum attribute value length, only used for attribute value.
    uint8_t *attrValue;     // attribute value, point to the actual attribute value.
    uint8_t settings;       // refer to ATT_SETTINGS_BITMAPS.
};

结合SDK给的Generic Access service的attribute table来说明每个参数的含义。代码见stack/ble/host_v1/services/svc_gatt/svc_core.c。

_attribute_data_retention_
static char defaultDevName[32] = DEFAULT_DEV_NAME;
_attribute_data_retention_
static uint16_t defaultDevNameLen = sizeof(DEFAULT_DEV_NAME) - 1;

_attribute_data_retention_
static uint16_t defaultAppearance = DEFAULT_DEV_APPEARANCE;
static const uint16_t defaultAppearanceLen = sizeof(defaultAppearance);

static uint16_t defaultPeriConnParameters[] = { 20, 40, 0, 100 };     //gap_periConnectParams_t
static const uint16_t defaultPeriConnParametersLen = sizeof(defaultPeriConnParameters);

/*
 * @brief the structure for default GAP service List.
 */
static const struct atts_attribute gapList[] =
{
    ATTS_PRIMARY_SERVICE(serviceGenericAccessUuid),

    //device name
    ATTS_CHAR_UUID_READ_POINT_NOCB(charPropRead, characteristicDeviceNameUuid, defaultDevName),

    //Appearance
    ATTS_CHAR_UUID_READ_ENTITY_NOCB(charPropRead, characteristicAppearanceUuid, defaultAppearance),

    //period connect parameter
    ATTS_CHAR_UUID_READ_ENTITY_NOCB(charPropRead, characteristicPeripheralPreferredConnParamUuid, defaultPeriConnParameters),
};

请注意,attribute table的定义前面加了static const。

static const struct atts_attribute gapList[] = {...};

1) perm

perm是permission的简写。

perm⽤于指定当前Attribute被Client访问的权限。

权限有以下10种,每个Attribute的权限都必须为下面的值或它们的组合。

#define ATT_PERMISSIONS_READ 0x01
#define ATT_PERMISSIONS_WRITE 0x02
#define ATT_PERMISSIONS_AUTHEN_READ 0x61
#define ATT_PERMISSIONS_AUTHEN_WRITE 0x62
#define ATT_PERMISSIONS_SECURE_CONN_READ 0xE1
#define ATT_PERMISSIONS_SECURE_CONN_WRITE 0xE2
#define ATT_PERMISSIONS_AUTHOR_READ 0x11
#define ATT_PERMISSIONS_AUTHOR_WRITE 0x12
#define ATT_PERMISSIONS_ENCRYPT_READ 0x21
#define ATT_PERMISSIONS_ENCRYPT_WRITE 0x22

⽬前Telink BLE Audio SDK暂不支持授权读和授权写。

2) uuid and uuidLen

按照之前所述,UUID分两种:BLE标准的2 bytes UUID和Telink私有的16 bytes UUID。通过uuid和uuidLen可以同时描述这两种UUID。

uuid是⼀个uint8_t型指针,uuidLen表示从指针开始的地⽅连续uuidLen个byte的内容为当前UUID。Attribute Table是存在flash上的,所有的 UUID也是存在flash上的,所以uuid是指向flash的⼀个指针。

a. BLE标准的2 bytes UUID

如device name uuid定义,相关代码如下:

#define CHARACTERISTIC_UUID_DEVICE_NAME                       0x2A00 //Device Name
const unsigned char characteristicDeviceNameUuid[ATT_16_UUID_LEN] = { U16_TO_BYTES(CHARACTERISTIC_UUID_DEVICE_NAME) };

b. Telink私有16 bytes UUID

如 OTA 的 Attribute,相关代码:

#define TELINK_SPP_DATA_OTA_V2        0x15, 0x2B, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00 //!< TELINK_SPP data for ota v2
static const uint8_t tlkSppOtaV2CharacteristicUuid[16] = { TELINK_SPP_DATA_OTA_V2 };

3) attrValueLen and attrValue

每⼀个Attribute都会有对应的Attribute Value。 pAttrValue是⼀个u8型指针,指向Attribute Value所在RAM/Flash的地址,attrValueLen⽤来反映该数据在RAM/Flash上的⻓度。当client读取server某个Attribute的Attribute Value时,BLE SDK从Attribute的pAttrValue指针指向的区域(RAM/Flash)开始,取attrValueLen个数据回给client。

UUID是只读的,所以uuid通常是指向flash的指针;⽽Attribute Value有可能会涉及到写操作,如果有写操作必须放在RAM上,所以pAttrValue可能指向 RAM,也可能指向 Flash。

当client写入server某个attribute的attribute Value时,该BLE SDK会判断该attribute有无写权限,并且判断写入的长度是否符合配置。如果符合所有要求的情况下,该BLE SDK会将写入的数据和长度存储到attrValueLen和AttrValue中。

  • attrValueLen是一个指向u16型指针,这意味在attribute value length是可变的长度。
  • attrValueLen如果是空指针,ATT层在读取属性值时,会返回长度0。

4) maxAttrLen

每一个attribute都会有对应的attribute Value,Value对应会有一个最大允许写入长度。当配置attribute value可以写入时,有两种情况:当写入长度为可变长度时,该BLE SDK允许小于等于maxAttrLen的配置写入到attrValue。为不可变长度时,该BLE SDK只允许等于maxAttrLEn的配置写入到attrValue。

5) setting

每一个attribute的需求可能都不一样,setting是为了满足不同的需求,可以定义BLE SDK对attribute的操作功能。目前可用的setting如下表:

Bit Name Description
0 ATTS_SET_WRITE_CBACK 如果service group有写回调,先运行写回调函数。
1 ATTS_SET_READ_CBACK 如果service group有读回调,先运行读回调函数。
2 ATTS_SET_VARIABLE_LEN Attribute value是否是可变长度,如果同时配置为允许写入。可变长度,允许小于等于 maxAttrLen的写操作;不可变长度,允许等于 maxAttrLen的写操作。
3 ATTS_SET_ALLOW_WRITE Attribute value是否允许写操作。
4-7 RFU 保留

Service Group:

为了实现server端可添加多个attribute table,LE Audio SDK定义了attribute group来实现这个功能。通过attribute group的加入,更高层可以在初始化或者运行过程中,将完成attribute table的添加/删除操作。

service group定义为:

struct atts_group {
    struct atts_group *pNext;           /** < Pointer to the next attribute group. */
    const struct atts_attribute *pAttr; /** < Pointer to the attribute services table in the group. */
    atts_r_cb_t readCallback;           /** < Pointer to the read callback function. */
    atts_w_cb_t writeCallback;          /** < Pointer to the write callback function. */
    uint16_t    startHandle;           /** < The start attribute handle of the group. */
    uint16_t    endHandle;             /** < The end attribute handle of the group. */
};

结合SDK给出的Generic Access Group来说明以上各项的含义。GAP(Generic Access Profile)的service group代码见:stack/ble/host_v1/services/svc_gatt/svc_core.c。

/*
 * @brief the structure for default GAP service group.
 */
_attribute_data_retention_
static struct atts_group svcGapGroup =
{
    NULL,
    gapList,
    NULL,
    NULL,
    GAP_START_HDL,
    0
};

请注意,attribute group的定义前面加了_attribute_ble_data_retention_。这个关键字会让编译器将变量svcGapGroup最终存到retention RAM里面,芯片进入sleep后,数据不会丢失。

如果想要节省retention RAM的资源,可用将这部分内存存储到flash中,只需要提前初始化pNext、readCallback、writeCallback参数即可实现同样的效果。将起始的Group指针存放到gAttributeGroup.head,尾Group指针存放到gAttributeGroup.tail中,queue的数量存放到gAttributeGroup.curNum即可。(不推荐采用)

SDK将所有的attribute group存在retention RAM中是为了适配所有需求,灵活配置。

1) pNext

pNext是指向下一个attribute group的指针,正常初始化,配置为NULL即可。在调用
ble_host_add_attribute_service_group或者ble_host_remove_attribute_service_group时,SDK会修改该指针。

2) pAttr

pAttr是指向attribute table的指针。

3) readCallback

Attribute table中所有attribute的读回调函数,配置了允许读回调的attribute才会触发。

回调函数readCallback是读函数。函数原型:

typedef int (*atts_r_cb_t)(uint16_t conn_handle, uint8_t opcode, uint16_t attr_handle, uint8_t **out_value, uint16_t *out_value_len);

user如果需要定义回调读函数,须遵循上面格式。回调函数 readCallback 是 optional 的,对某⼀个具体的 Attribute group来说,user 可以设置回调读函数,也可以不设置回调(不设置回调的时候用空指针NULL表示)。

回调函数readCallback触发条件为:当server端收到的Attribute PDU的 Attribute Opcode为以下五个时,server会检查回调函数readCallback是否被设置:

a) opcode = 0x0A, Read Request.

b) opcode = 0x0C, Read Blob Request.

c) opcode = 0x08, Read By Type Request. (通常不会由这个opcode触发,协议上是允许的)

d) opcode = 0x0E, Read Multiple Request.

e) opcode = 0x20, Read Multiple Variable Request.

server收到以上读命令后:

a) 如果 user 设置了回调读函数,执⾏该函数,根据该函数的返回值决定是否回复:

  • 若返回值为0x01-0xFF(enum attribute_error_code),server回复error response给client,attribute handle in error设置为request里面的handle,error code设置为返回值。

  • 若返回值为ATT_SUCCESS,且out_value_len为0时,server从attrValue指针所指向的区域读attrValueLen个值回复给client。

  • 若返回值为ATT_SUCCESS,且out_value_len不为0时,server会从out_value指针所指向的区域读取数据,回复给client。如果out_value_len长度大于MTU时,会自动拆分为符合要求的数据,out_value必须是全局变量

  • 若返回值为其他值时,server不做任何操作,默认user会回复正确的响应。

b) 如果user没有设置回调读函数,server从pAttrValue指针所指向的区域读attrLen个值回复给client。

4) writeCallback

Attribute table中所有attribute的写回调函数,配置了允许写回调的attribute才会触发。

回调函数writeCallback是写函数。函数原型:

typedef int (*atts_w_cb_t)(uint16_t conn_handle, uint8_t opcode, uint16_t attr_handle, uint8_t *value, uint16_t value_len);

user如果需要定义回调写函数,须遵循上⾯格式。回调函数writeCallback是 optional的,对某⼀个具体的Attribute Group来说,user可以设置回调写函数,也可以不设置回调(不设置回调的时候⽤空指针NULL表⽰)。

回调函数writeCallback触发条件为:当server收到的 Attribute PDU 的 Attribute Opcode 为以下三个时,server会检查回调函数writeCallback是否被设置:

a) opcode = 0x12, Write Request.

b) opcode = 0x52, Write Command.

c) opcode = 0x18, Execute Write Request.

server收到以上写命令后:

a) 如果 user 设置了回调写函数,执⾏该函数,根据该函数的返回值决定是否回复Write Response/None/Execute Write Response:

  • 若返回值为0x01-0xFF(enum attribute_error_code),server回复error response给client,attribute handle in error设置为request里面的handle,error code设置为返回值。

  • 若返回值为0,SDK会根据setting和maxAttrLen参数,执行写attribute value操作。

  • 若返回值为其他值时,server不做任何操作。

b) 如果user没有设置回调写函数,SDK会根据setting和maxAttrLen参数,执行写attribute value操作。

5) startHandle

该attribute group包含的attribute table的起始句柄。

6) endHandle

该attribute group包含的attribute table的结束句柄。

Attribute handle 分配:

由于现在Attribute table + service group的方式,LE Audio SDK将Attribute Handle做了一些预分配。如果user有用到SDK定义的service group需要注意attribute handle的分配问题。

SDK将现在使用到的Attribute Handle的定义存放在stack/ble/host_v1/services/svc.h。SDK将attribute handle大致划分如下表:

UUID 长度 Service 类型 Start Handle End Handle
16bit UUID GATT service 0x0001 0x00BF
HID service 0x00C0 0x00FF
Other Service 0x0100 0x01FF
Audio service 0x0200 0x07FF
RAS service 0x0800 0x087F
RFU service 0x0880 0x3FFF
user service 0x4000 0x7FFF
128bit UUID Telink service 0x8000 0x8FFF
user service 0x9000 0xFFFF

user如果有自定义的uuid时,最好放在user service区域,按照16bit/128bit UUID类型分配,这样可以加快client查询service速度。

ATT opcode支持情况:

该SDK已经支持除signed write command以外的所有ATT packet。

/**
 *   @brief this function is used to register ATT, SMP, signaling and callbacks.
 *
 *   @param[in] cid: channel ID, LE_L2CAP_CID_ATT, LE_L2CAP_CID_SIGNALING, LE_L2CAP_CID_SMP.
 *   @param[in] ctrl_callback: control callback function.
 *   @param[in] data_callback: data callback function.
 *
 *   @return none.
 */
void ble_host_l2cap_register_callbacks(uint8_t cid, l2cap_ctrl_callback_t ctrl_callback, l2cap_data_callback_t data_callback);

ATT Service

ATT Service的使用方法可以参考ble example的BLE SPP Server Demo里面的使用方法。

BLE SMP

模块概览:

Security Manager Protocol(SMP)是BLE安全的核心,负责蓝牙设备间的配对认证、密钥分发和隐私地址管理,实现数据加密传输以及保护用户隐私。SMP 安全模型定义了如下主要安全等级(Security Levels):

  • Security Mode 1 Level 1:无认证,数据未加密(No authentication, no encryption),仅用于极简连接;
  • Security Mode 1 Level 2:未认证但加密(Unauthenticated pairing with encryption),如Just Works、未认证的OOB/Passkey;
  • Security Mode 1 Level 3:已认证并加密(Authenticated pairing with encryption),如认证的Passkey、OOB配对等;
  • Security Mode 1 Level 4:基于LE Secure Connections的认证加密(Authenticated LE Secure Connections pairing with encryption),采用ECDH算法,实现更高的安全性。

SMP协议支持两种主要配对方式:

  • Legacy Pairing(传统配对):兼容BLE 4.0设备,支持上述1~3级安全等级,配对基于预共享TK,易受中间人攻击,安全性有限。
  • LE Secure Connections(安全连接):自BLE 4.2起引入,采用ECDH公私钥协商,支持1~4级安全等级(即secure模式下仍可协商较低安全级别,具体取决于双方I/O能力、配对流程等),显著提升抗攻击能力并支持Numeric Comparison、Passkey、OOB及Just Works多类方法。

本SDK完全支持SMP协议的全部安全等级与配对流程,兼容Legacy与Secure两种模式,并且适配Central / Peripheral全角色应用场景。开发者可通过SDK提供的API灵活选择所需的配对交互方式和安全等级,SDK底层会自动根据场景选择支持的最高等级与最适配IO交互流程,实现从简单配对到高安全防护的全覆盖BLE连接。

头文件介绍:

头文件 作用 典型接口/结构
inc/ble_smp.h 对外 API & 回调定义 ble_host_smp_initial()
ble_host_smp_set_passkey()
ble_host_smp_set_oob_value()
struct ble_host_smp_callbacks
BLE_HOST_SMP_*_INIT_PARAMS
inc/ble_smp_store.h 绑定数据访问接口 ble_host_smp_store_init()
ble_host_smp_store_get_key()
可用于自定义配对记录管理

SMP使用流程示例:

SMP配对/安全相关的所有实现逻辑已封装在SDK内部,应用无须关注具体细节,仅需参考例程(BLE Example的SMP)的使用方法即可实现传统/安全配对。

以下结合example代码,简述常用SMP使用流程:

  • 初始化与回调注册:参考example (vendor/ble_example/app_acl_smp/app_acl_smp.c) 初始化SMP以及注册配对回调。例如:
// select pairing mode, legacy pairing just works
ble_host_smp_initial(
    BLE_HOST_SMP_LEGACY_JUST_WORKS(
        app_acl_smp_pairing_started_callback,
        app_acl_smp_pairing_finish
    )
);
// or secure connection pairing passkey input
ble_host_smp_initial(
    BLE_HOST_SMP_SC_PASSKEY_INIT_INPUT(
        false,
        app_acl_smp_pairing_input_callback,
        app_acl_smp_pairing_output_callback,
        app_acl_smp_pairing_started_callback,
        app_acl_smp_pairing_finish
    )
);

SDK 会自动注册底层 GAP/L2CAP 通道和所需回调,无需用户手动干预内部函数。

  • 设置隐私和 IRK(可选):若需开启隐私广播或周期变换地址,直接调用公开 API 生成 RPA:
uint8_t rpa[6];
ble_host_smp_generate_resolvable_private_addr(rpa);
// 可用于设置广播地址等场景
  • 配对方法和状态机选择:用户只需选择合适的初始化宏(Just Works/Passkey/OOB/SC 等),底层会根据协议和 IO 能力自动选择配对流程。典型流程详见 Demo 配对模式说明,无需关注代码内部映射表。

  • 配对记录与密钥存储:若设备支持多主机记忆、解绑和自动重连,需初始化绑定信息存储队列,并结合 UI 菜单实现“忘记设备”等。协议栈不区分 Central 和 Peripheral,统一使用绑定信息存储队列,目前 SDK 平台只支持 8 个配对数量,这是 SDK 存储模块限制,不是 host 限制。

ble_host_smp_store_init(3, 2); // 支持最多3个Peripheral和2个Central的绑定信息存储
// 查询、删除见 inc/ble_smp_store.h 提供的接口
  • Tips
    • 所有 SMP 功能均可参考 example 的调用方式实现。
    • 示例 demo 提供了典型配对流程与 UI 输入/输出回调接入样板,直接复用或按需修改即可。

如需更详细实际代码,可参考 vendor/ble_example/app_acl_smp 目录下的配对 demo,实现全流程配对和密钥管理。

API 速查:

  • ble_host_smp_initial() / ble_host_smp_deinit():启动/关闭 SMP。
  • ble_host_smp_set_passkey()ble_host_smp_set_oob_value():在配对回调中填写 TK/OOB。
  • ble_host_smp_generate_resolvable_private_addr():生成可解析私有地址,用于隐私广播或周期刷新。
  • ble_host_smp_store_*:查询、写入或删除绑定记录,可与用户 UI 结合实现“忘记设备”功能。

Demo - app_acl_smp

vendor/ble_example/app_acl_smp 提供了完整的 ACL + SMP 演示,可通过 APP_ACL_SMP_SELECT_MODE 一键切换配对形态。

vendor/ble_example/app_acl_smp/app_acl_smp.c
static void app_acl_smp_init_module(void)
{
#if APP_ACL_SMP_SELECT_MODE == APP_ACL_SMP_MODE_LEGACY_JUST_WORKS
    ble_host_smp_initial(BLE_HOST_SMP_LEGACY_JUST_WORKS(app_acl_smp_pairing_started_callback, app_acl_smp_pairing_finish));
#elif APP_ACL_SMP_SELECT_MODE == APP_ACL_SMP_MODE_LEGACY_PASSKEY
    ble_host_smp_initial(BLE_HOST_SMP_LEGACY_PASSKEY_INIT_INPUT(false, app_acl_smp_pairing_input_callback, app_acl_smp_pairing_output_callback,
        app_acl_smp_pairing_started_callback, app_acl_smp_pairing_finish));
...
#elif APP_ACL_SMP_SELECT_MODE == APP_ACL_SMP_MODE_SECURE_CONNECTION_PASSKEY
    ble_host_smp_initial(BLE_HOST_SMP_SC_PASSKEY_INIT_INPUT(false, app_acl_smp_pairing_input_callback, app_acl_smp_pairing_output_callback,
        app_acl_smp_pairing_started_callback, app_acl_smp_pairing_finish));
#endif
}
  • 选择 Demo:在 app_example.h 中将 APP_DEMO_SELECT 设为 APP_BLE_ACL_SMP,并在 app_acl_smp.c 调整 APP_ACL_SMP_SELECT_MODE
  • 回调处理app_acl_smp_pairing_started_callback()app_acl_smp_pairing_finish() 负责打印状态;若模式包含输入/输出能力,会额外注册 app_acl_smp_pairing_input_callback() / app_acl_smp_pairing_output_callback(),默认通过日志或 USB shell 交互。
  • 调试命令:当需要输入手机显示的 PIN 码时,可在 RISC‑V TDB 里输入 11 xx xx xx(十六进制 BCD,XX XX XX为对应 PIN 码),tlkusb_debug_shell_hook() 会解析后调用 ble_host_smp_set_passkey() 完成回填。

BLE GATT

GATT 简介:

GATT(Generic Attribute Profile,通用属性配置文件)是 BLE 协议中实现服务发现、描述及操作的核心协议。GATT 基于 ATT(Attribute Protocol),定义了数据如何被组织、传输以及如何进行服务的发现与描述。

GATT 相关文件目录说明:

SDK 中的 GATT 相关源码文件stack/ble/host_v1/gatt 目录:

  • gatt/ :实现了 GATT 相关协议操作:如服务发现、读写 characteristic/descriptor、notification/indication 发送等。

GATT 核心规范架构:

根据 Bluetooth Core Specification,GATT 定义了两种角色:

  • GATT Server:定义并管理某个服务的属性,并响应 Client 的 ATT 请求。
  • GATT Client:发起请求,从 Server 处发现服务、特性以及读取/写入对应的属性。

GATT 的核心机制包括:

  • 服务(Service)/特性(Characteristic)/描述符(Descriptor)三大组织结构。
  • 每个 attribute 都有唯一的 handle (16bit),客户端访问以 handle 为单位。
  • 关键操作包括:读、写、通知(Notification)、指示(Indication)、发现(Discovery)、配置(Configure)等。

GATT 文件夹主要功能点梳理

  • 数据通信能力:支持所有标准 GATT 操作,如 Read, Write, Write Command, Notification, Indication, Prepare/Execute Write 等。
  • 回调机制:透过 read/write callback 技术,允许用户自定义属性读写处理逻辑与安全验证机制。
  • 发现流程兼容:支持标准 GATT 客户端发现、遍历服务/特性,保证与主流手机/设备互通。

BLE GATT Server:

BLE GATT Server(gatts)模块负责定义、注册并维护 BLE 属性表,决定 GATT 数据(Service、Characteristic、Descriptor)结构,并响应客户端的各类 ATT/GATT 请求。Telink BLE SDK 支持自定义 GATT 服务器,能够实现服务发现、特性(Characteristic)的读写、以及关键的数据推送能力(如 Notification/Indication)。

GATTS 主要功能:

  • 服务发现与读写响应:自动响应 GATT Client 的服务/特性发现请求、属性读写命令,并允许应用实现自定义的读写回调(回调里可处理权限、数据动态生成等场景)。

  • Notification & Indication 支持:支持标准 BLE Notification(无须 client 确认)和 Indication(client 需确认)机制,可用于主动将数据推送到远端客户端(如 App、小程序等)。

Notification & Indication 特性说明:

  • Notification of a Characteristic Value:Notification 用于 Server 主动推送 characteristic value 给 Client,无需 Client 回复。通常用于上报传感器数据、推送状态,延时小、效率较高。

调用 SDK API 形式举例:

// 向已使能 notify 的 client 主动推送数据
ble_gatts_notify(conn_handle, char_handle, p_data, len);
  • Indication of a Characteristic Value:Indication 同样为 Server 主动推送,但 Client 必须返回确认(Handle Value Confirmation)。适合关键信息同步。

调用 SDK API 形式举例:

// 向已使能 indicate 的 client 主动推送数据(需等待 client 确认)
ble_gatts_indicate(conn_handle, char_handle, p_data, len, cb);
  • 使能 Notification/Indication 流程:GATT Client 需先写入 CCCD 属性(0x0001 开启 notify,0x0002 开启 indicate),服务器端收到写入后才能开始下发。

常用接口说明:

GATT Feature SDK 接口示例 说明
Notification ble_gatts_notify()
ble_gatts_notify_with_callback()
主动向 client 推送 notify 数据,可选回调获知发送结果。
Indication ble_gatts_indicate() 主动向 client 推送 indication 数据,SDK 在 client 确认后触发回调。

简单应用举例:

  • 实现 Notification/Indication 推送

    • 当需要向 app 主动汇报数据(如传感器采样值)时,检测 client 是否已 enable(可通过 CCCD 配置),如已 enable,则调用相应接口推送数据给 client。
    • Notification 适合高频数据、延迟敏感场景;Indication 适合重要信息同步与事务性反馈。
  • 处理 CCCD 变化

    • 在自定义 write 回调中,检测 CCCD 的 write 行为,可记录哪些客户端/连接已经打开了 notification 或 indication 功能。
    • 推送数据前建议检查对应 client 的 ccc 配置。

BLE GATT Client:

BLE GATT Client(GATTC)模块主要负责以客户端身份与远端 BLE GATT Server 交互,如发现服务/特性、读取/写入属性、处理通知与指示等。Telink BLE SDK 中 GATT Client 的相关接口定义在 gattc/inc/gattc_req.h 文件中。

GATTC 主要功能:

  • 服务与特性发现
    • GATTC 提供标准的服务(Service)、特性(Characteristic)、描述符(Descriptor)发现接口,支持全服务树遍历或指定 UUID 定向查找。
  • 属性读写操作
    • 支持以 handle 或 UUID 对服务端属性进行读(Read)、写(Write)、写命令(Write Without Response)、预写(Prepare Write)等,并能灵活组合典型 BLE 读写流程。
  • Notification/Indication 处理
    • 提供设置和接收服务器端的 Notification/Indication 数据通道,便于实现将数据推送到客户端的典型 BLE 场景(如心率、血糖计等通知型设备)。

关键接口举例:

int ble_host_gattc_read_characteristic_value(uint16_t conn_handle, uint16_t cid, const struct gattc_read_characteristic_value_param *param);

通过 handle 发起 Read Characteristic Value 子过程,param 中携带读操作回调与 user data。

int ble_host_gattc_write_characteristic_value(uint16_t conn_handle, uint16_t cid, const struct gattc_write_characteristic_value_param *param);

带 Response 的写请求,SDK 会在写完成后通过回调反馈结果,适合关键属性改写。

int ble_host_gattc_write_characteristic_value_without_response(uint16_t conn_handle, uint16_t cid, uint16_t handle, const uint8_t *buffer, uint16_t length);

Write Command(无响应)流程,适合高频或实时性要求高的写入。

int ble_host_gattc_discover_all_primary_services(uint16_t conn_handle, uint16_t cid, gattc_disc_service_callback callback, void *user_data);

遍历远端所有 Primary Service;发现结果通过 callback 逐条返回。

int ble_host_gattc_discover_primary_service_by_uuid(uint16_t conn_handle, uint16_t cid, const struct att_uuid *service_uuid, gattc_disc_service_callback callback, void *user_data);

依据指定 UUID 精准查找目标 Service。

int ble_host_gattc_discover_all_characteristics_of_service(uint16_t conn_handle, uint16_t cid, const struct gattc_disc_all_characteristics *param);

在给定 handle 区间内发现全部 Characteristic,参数结构体包含范围与回调。

int ble_host_gattc_discover_all_characteristics_of_service_by_uuid(uint16_t conn_handle, uint16_t cid, const struct gattc_disc_all_characteristics_by_uuid *param);

使用 UUID 做特性发现过滤。

int ble_host_gattc_discover_characteristic_desc(uint16_t conn_handle, uint16_t cid, const struct gattc_disc_characteristic_desc_param *param);

遍历特性下所有 Descriptor(含 CCCD、User Description 等)。

int ble_host_gattc_write_ccc_value_enable_notify(uint16_t conn_handle, uint16_t cid, uint16_t handle, gattc_write_characteristic_value_callback callback, void *user_data);

写入 CCCD 以开启 Notification;Indication、关闭等亦有对应 write_ccc_value_* 变体接口。

GATT Profile Feature 与 SDK 接口映射:

GATT Feature SDK 接口示例 说明
1. Server Configuration ble_host_gattc_send_exchange_mtu_req() 通过 MTU 交换完成 Client/Server 之间的基础配置。
2. Primary Service Discovery ble_host_gattc_discover_all_primary_services()
ble_host_gattc_discover_primary_service_by_uuid()
发现全部 Primary Service 或按 UUID 精准查询。
3. Relationship Discovery ble_host_gattc_find_included_services() 枚举 Service 之间的包含关系,定位 Included Service。
4. Characteristic Discovery ble_host_gattc_discover_all_characteristics_of_service()
ble_host_gattc_discover_all_characteristics_of_service_by_uuid()
获取服务下所有特性或按 UUID 定位特性句柄。
5. Characteristic Descriptor Discovery ble_host_gattc_discover_characteristic_desc() 遍历特性下的 Descriptor(包含 CCCD、User Description 等)。
6. Reading a Characteristic Value gatt_client_read_by_handle()
ble_host_gattc_read_characteristic_value()/_long/_using_uuid
覆盖短读、长读、多值读、UUID 定向读等子流程。
7. Writing a Characteristic Value gatt_client_write_with_rsp()
gatt_client_write_cmd()
ble_host_gattc_write_characteristic_value()
支持有响应写、无响应写、长写与可靠写。
8. Characteristic Value Notification gatt_client_cfg_notify()
ble_host_gattc_write_ccc_value_enable_notify()
通过写 CCCD 打开 Notification,下行数据由回调接收。
9. Characteristic Value Indication ble_host_gattc_write_ccc_value_enable_indicate() 同理配置 Indication,SDK 自动处理确认流程。
10. Reading a Characteristic Descriptor ble_host_gattc_read_characteristic_descriptor()
ble_host_gattc_read_long_characteristic_descriptor()
读取短/长 Descriptor,常用于读取 CCCD 初始值等。
11. Writing a Characteristic Descriptor ble_host_gattc_write_characteristic_descriptor()
ble_host_gattc_write_long_characteristic_descriptor()
设置 Descriptor 内容,含短写、长写两类操作。

注意

  • 以上接口均定义于 gattc/inc/gattc_req.h,可结合不同回调和参数结构完成业务编排。

应用举例:

  • 发现服务与特性(Service/Characteristic Discovery)

    • 一般在连接建立后,BLE Client 会调用 ble_host_gattc_discover_all_primary_services()ble_host_gattc_discover_all_characteristics_of_service() 等接口遍历目标 Server,获取 service/characteristic/descriptor 的 handle 列表,为后续 read/write/notify 做准备。
  • 属性读写操作

    • 拿到 handle 后,Client 可使用 ble_host_gattc_read_characteristic_value()(读)、
      ble_host_gattc_write_characteristic_value()
      ble_host_gattc_write_characteristic_value_without_response()(写)等流程组合常见业务。
  • 通知/指示配置

    • 对支持 Notification/Indication 的特性,需通过 ble_host_gattc_write_ccc_value_enable_notify() / ble_host_gattc_write_ccc_value_enable_indicate() 写入对应的 CCCD,Server 便会开始通过 Notification/Indication 主动推送数据,SDK 回调可用于接收与处理。

实现架构与回调机制:

GATTC 内部抽象了 GATT Client 状态机与队列,支持多连接、多操作多路并发,所有操作都按 GATT 协议栈规定有精确超时与流程回调。用户只需订阅 gattc 相关事件和回调,即可在自己的业务代码中专注数据处理。

参考文档:

可直接查阅《Bluetooth Core Specification》/Vol 3/Host/Part G/Chapter 4 ,以及 gattc_req.h 中的 API 注释,具体的效果ble example的BLE SPP Client Demo。

TPSLL 应用相关

TPSLL(Telink Proprietary Synchronous Link Layer)是Telink基于2.4G私有协议开发的音频混音类应用的底层链路的统称;在Bluetooth audio SDK中,主要应用在BT/TPSLL TWS参考设计,BT/TPSLL Headset参考设计,TPSLL Audio dongle参考设计中。由于其灵活的协议设计,在音频类应用开发中,具有天然的功耗和距离,低延时等等优势;很好地弥补了其他标准协议诸多不足之处,给用户带来了诸多极致地性能体验。

主要特性如下

  • 链路配对
  • 链路回连
  • 链路管理
  • 跳频机制
  • 应答机制
  • 重传机制
  • 同步机制
  • 链路虚拟机制
  • 同步数据传输机制
  • 异步数据传输机制
  • 组包和分包机制

该部分属于底层链路的内容,不对外公开,用户不需要深入了解,关于应用部分的接口和具体使用方式,可以参照第7章中关于 BT/TPSLL HeadsetBT/TPSLL TWS参考设计。

应用相关

各个项目工程对应的硬件环境及相关说明参见tl_bluetooth_audio_sdk Get Started

BT/BLE Headset

概述

本章节主要对 Bluetooth Audio SDK 中 BT/BLE Headset 参考设计进行介绍。 BT/BLE Headset 是针对蓝牙头戴式耳机等产品的音频应用,基于 Telink SDK 实现,提供双模音频并发、低功耗等功能。支持经典蓝牙(A2dP/HFP) 与 LE Audio(Unicast Server) 音频功能,覆盖主流手机/PC等产品的连接。

主要特性如下:

  • 经典蓝牙相关特性

    • 支持BT链路配对,回连,角色切换等功能
    • 支持安全加密(SSP),自适应跳频(AFH)
    • 支持SNIFF模式
    • 支持经典蓝牙双连接
  • LE Audio相关特性

    • 支持 Unicast Server 音频功能
    • 支持 LC3 编解码
    • 支持 48 kHz 音乐播放
    • 支持 32 kHz 双向通话
    • 支持 BLE HID Keyboard 应用
    • 支持 GMCS、CCP协议,多媒体控制和电话控制
    • 支持 16 kHz、24 kHz、32 kHz、48 kHz 音频采样率
    • 支持 7.5 ms、10 ms 音频帧间隔
    • 支持 Secure Connection 加密
  • 经典蓝牙和LE Audio共存特性

    • 支持经典蓝牙和LE连接共存。
    • 支持 LE Audio 音乐/通话模式,同时与一路BT连接共存。
    • 支持 LE Audio 音乐/通话模式,同时与一路BT音乐共存。

目录结构

btble_headset
├── app_ble_headset.c   // BLE Unicast Server Headset.
├── app_ble_hid.c   // BLE HID Keyboard.
├── app_ble.c       // BLE common functions.
├── app_bt.c       // BT ACL, profile connection, disconnection event and reconnection processing module.
├── app_config.h    // Engineering configuration file
├── app_key_led_config.c    // Key configuration file
├── app.c 
└── main.c  // Project entrance

LE Headset 的默认名称是 LE Headset-XXXXXXXXXXXXX,其中 X 是设备的 MAC 地址。

用户可以通过修改宏定义,来实现名字的修改:

#define LE_HEADSET_DEVICE_NAME          "LE headset"

BT Headset 的模式名称是 Telink-BT-XX:XX:XX:XX:XX:XX。

可以通过日志查看实际的设备名称,如下图所示:

Bluetooth Name

BT Host

下文统一给出应用层 BT Host 在 Vendor 中的接口全集,涵盖 ACL 及 Profile 连接/断连等关键事件;后续各应用参考设计凡涉及 BT 部分,均与此处完全一致,不再重复说明。

  • 注册 ACL 连接/加密/断开回调,处理配对信息、重连、扫描策略;
static void app_btmgr_aclConnectCB(uint16 handle, uint08 status, uint08 *pBtAddr, uint08 dtype, uint08 hfp_ChId)

该函数是 ACL 连接完成事件(BTH_EVTID_ACLCONN_COMPLETE)在应用层的回调处理函数。主要用于通知上位机 ACL 连接事件的产生、结束配对和 scan 动作。

static void app_btmgr_aclDisconnCB(uint16 handle, uint08 reason, uint08 *pBtAddr)

该函数是 ACL 断开完成事件(BTH_EVTID_ACLDISC_COMPLETE)在应用层的回调处理函数。主要用于通知上位机 ACL 断开事件的产生、提示音/LED 等 UI 状态的更新、音频调度器资源回收和 ACL 超时掉线(BTH_HCI_ERROR_CONN_TIMEOUT)时回连动作的触发。

static void app_btmgr_aclEncryptCB(uint16 handle, uint08 status, uint08 *pBtAddr, uint08 dtype, uint08 hfp_ChId)

该函数是链路加密完成事件(BTH_EVTID_ENCRYPT_COMPLETE)在应用层的回调处理函数。主要用于发起 SDP 服务查询、追加要连接的 Profile 和使能 BT sniff 功能。在当前 SDK 设计中,所有 Profile 的连接动作都发生在加密完成后。

static void app_btmgr_ProfConnCB(uint16 handle, uint08 status, uint08 ptype, uint08 usrID, uint08 *pBtAddr, uint08 isFirstProf)

该函数是 profile 连接完成事件(BTP_EVTID_PROFILE_CONNECT)在应用层的回调处理函数。主要用于提示音/LED 等 UI 状态的更新、音频调度器预装载。

static void app_btmgr_ProfDiscCB(uint16 handle, uint08 reason, uint08 ptype, uint08 usrID, uint08 *pBtAddr)

该函数是 profile 断开完成事件(BTP_EVTID_PROFILE_DISCONN)在应用层的回调处理函数。主要用于音频调度器资源回收、追加要连接的 profile(该动作仅在 SDP 以 client 角色查询结束后发生)。

  • 根据设备类型与 TinySQL 中保存的 Profile 信息,动态追加 A2DP/HFP/PBAP/IAP/AVRCP/ATT 等 Profiles,确保多协议并行。
static void app_btmgr_appendProfile(uint16 aclHandle)

该函数主要用于追加要连接的 profile,函数内部通过 tlkmdi_btacl_appendProf() 将具体的某一个 profile 追加到 profile 资源管理器中,在加密结束后根据 delayMs依次主动向对端设备发起 profile 连接。

  • 通过 tlkmdi_btSet_scan()tlkmdi_btRecon_start() 控制扫描与回连节奏。
static void app_btmgr_poweron_action(void)

该函数主要用于设备开机上电后回连上次配对过的 BT 设备。如果 flash 中保存了设备的配对信息调用 tlkmdi_btRecon_start() 发起回连,否则调用 tlkmdi_btSet_scan() 进入配对模式。关于 scan 和 reconnect 这两部分内容在 scan 管理和回连管理章节中有详细介绍,此处不再赘述。

LE Host

BLE HID Device 注册了BLE HID相关的所有功能。

void app_ble_hid_init(void);

需要在app_config.h中配置宏定义,打开BLE HID相关的功能。

#include "stack/ble/host_v1/services/svc_hid/hid_demo/keyboard_cfg.h"

BLE Unicast Headset 注册了 LE Audio 相关的所有功能,用户只需要传入蓝牙名称,广播间隔,默认音量大小。具体的参考代码中的实现。

struct lea_us_headset_param {
    const char *device_name; /** < Advertising/display name. */
    uint16_t    interval;    /** < Extended advertising interval in milliseconds. */
    uint8_t     volume;      /** < Initial render volume (0~255). */
};
void lea_unicast_server_headset_initial(const struct lea_us_headset_param *param);

需要在app_config.h中配置宏定义,打开LE Audio Path相关的功能。

#define TLK_MW_LEA_US_MUSIC_ENABLE   1
#define TLK_MW_LEA_US_VOICE_ENABLE   1
#define CODEC_MIC_FIFO_SAMPLES          2048
#define LE_AUDIO_CODEC_INPUT_TYPE    LE_AUDIO_CODEC_TYPE_CODEC
#define LE_AUDIO_CODEC_OUTPUT_TYPE   LE_AUDIO_CODEC_TYPE_CODEC
#define APP_AUDIO_ASCSS_SINK_ASE_CNT 1
#define APP_AUDIO_ASCSS_SRC_ASE_CNT  1

按键功能

按键号 单击 双击 三击
KEY1 播放/暂停音乐 下一曲 开启配对
KEY2 接听最新的来电。
1. 之前没有电话的时候,接听当前通话。
2. 之前已经有通话的时候,接听最新的通话,之前电话等待。
上一曲
KEY3 音量+ 音量-
KEY4 音量+(BLE_HID) 音量-(BLE_HID) 拒接新的通话
1. 之前没有电话的时候,拒接当前来电
2. 之前已经有电话的时候,拒接三方来电

BT/BLE 音源

功能概览

双模 Bluetooth Classic + Bluetooth Low Energy Audio(LE Audio)USB Dongle,用于 PC / 笔记本 / 主机 等设备,将音频通过蓝牙无线发送到:

  • 🎧传统蓝牙耳机(BT Classic / A2DP)

  • 🎧新一代 BLE Audio 耳机(LE Audio / LC3)

解决 PC 等设备原生不支持 BLE Audio,或需要同时兼容老蓝牙耳机和新 LE Audio 耳机的问题。

当前支持以下功能场景:

(1)BT Classic 连接

(2)BT Classic 音乐

(3)BT Classic 通话

(4)LE Audio 连接

(5)LE Audio 音乐

(6)LE Audio 通话

(7)BT与BLE双连接共存(音频不共存)

功能流程图

Audio Source Flowchart

软件架构

(1) BLE

BLE Audio 处理位于tlkmw\ble\le_audio\lea_unicast_client.c中。

  • lea_unicast_client_start

初始化unicast client profile服务,gap接口等,并默认开启扩展扫描(2分钟)。

  • lea_unicast_client_ext_scan_handler

该函数是 BLE Audio Unicast Client 在扩展扫描(Extended Scan) 阶段的广播数据处理回调函数。它用于在扫描到 BLE 外设广播包后:解析并校验 BLE Audio 相关广播数据,筛选可发现的 LE Audio 设备,向上层上报扫描到的设备信息,根据已绑定信息(Bond)或 SIRK 信息,自动发起连接或回连。

  • 解析广播数据(LTV 格式)
    • 广播数据采用 LTV(Length-Type-Value) 格式ltv_unpack() 会遍历所有 AD Type每解析到一个 AD Type,就调用lea_unicast_client_check_adv_data_handle()解析结果被填充到 adv_data 结构体中,包括:LE Audio 标志,Discoverable Flags,设备名,RSI(Resolvable Set Identifier),其他 CAP / CSIS 相关信息,若解析失败,直接忽略该广播包:
struct cap_device_adv_data_value adv_data = { 0 };
const int ret = ltv_unpack(data, data_len, lea_unicast_client_check_adv_data_handle, &adv_data);

if (ret != LTV_UNPACK_SUCCESS) {
    return;
}
  • 判断是否为可发现的 LE Audio 设备
    • 判断条件包含 LE Audio 广播标志以及处于Limited Discoverable Mode 或General Discoverable Mode,即:这是一个可被发现的 LE Audio 设备,此时会插入扫描表并发送到上位机进行显示。
if (adv_data.lea_audio_flags && adv_data.flags & (FLAGS_LE_LIMITED_DISCOVERABLE_MODE | FLAGS_LE_GENERAL_DISCOVERABLE_MODE)) {
    if (!cap_device_insert_adv(addr_type, addr, &adv_data)) {
        tlkapp_lemgr_sendExtScanDataEvt(addr_type, addr, (uint8_t *) adv_data.complete_name, adv_data.complete_name_len);
    }
}
  • 查询是否存在已绑定(Bond)信息
    • 查询 SMP 存储区,获取该设备是否已配对,pairing_index != 0 表示存在 Bond 信息。
struct ble_host_smp_store_key *p_store_key = ble_host_smp_store_get_pairing_info(addr_type, addr);
uint32_t pairing_index = p_store_key != NULL ? p_store_key->pairing_index : 0;
  • 基于 Bond 信息或 SIRK 进行回连
    • if (pairing_index > 0 && tlkapp_lemgr_GetAutoRec())用于判断通过绑定信息以及是否开启自动回连进行回连。if (audio_device_context.sirk_flag)用于通过判断SIRK回连第二路TWS耳机。
if (pairing_index > 0 && tlkapp_lemgr_GetAutoRec()) {
        // 自动回连
} else if (audio_device_context.sirk_flag) {
        // SIRK 回连第二路TWS耳机
}
  • lea_unicast_client_sdp_flags_event_handler
    • 该函数是 BLE Audio Unicast Client 在连接建立后,用于处理服务发现(SDP / Profile Discovery)事件 的回调函数。
    • 将 SDP 发现结果同步给 CAP 设备管理模块,在 SDP 完成后进行必要的初始化配置(如音量),从已发现的设备中提取 CSIS 的 SIRK 信息,更新音频设备上下文状态等。
  • lea_unicast_client_cap_device_state_cb
    • 该函数是CAP设备连接断连回调,当有BLE音频设备连接或者所有BLE音频设备断开时触发,连接回调进行音频任务添加,断连回调进行音频任务删除。
  • lea_unicast_client_connected_callback
    • BLE ACL连接成功回调,连接成功进行SMP、MTU交互等流程。
  • lea_unicast_client_disconnected_callback
    • BLE ACL断开连接回调,断开连接后会自动开启扩展扫描(2分钟)。

(2) BT

  • 参考BT/BLE Headset章节BT Host部分。

(3) 软件环境

使用Telink IDE编译SDK,烧录当前demo固件后可以通过上位机(参考上位机章节使用)进行操作以及log显示,上位机使用串口进行通信,串口波特率默认为1500000。当前上位机支持BT/BLE扫描连接断连,清空配对表等功能。

UART Tool

(4) 操作步骤

USB连接:

开发板需要使用USB连接,通过UAC将电脑音频与开发板音频连接,在电脑端需要选择UAC模拟出的音频设备。

Audio Source UAC

串口连接:

连接串口,使用上位机进行操作,相关使用参考上位机章节。

  • 扫描BT/BLE设备

点击start searchopen ble scan进行对应音频设备的查询

Audio Source Scan

  • 连接BT/BLE设备

扫描到的设备会显示在搜索列表以及扫描列表中,选中想要连接的设备,通过点击连接BT设备或连接BLE设备进行连接,连接成功后会在已连接列表中显示。

Audio Source Connection

  • 断开BT/BLE设备

在已连接列表中选中想要断开的设备,点击断开按钮进行断开。

  • 获取、设置BT名称或BT地址

点击获取 BT 名称或 BT 地址,可以获取当前设备的名称或 MAC 地址。修改 BT名称或 BLE地址,点击设置 BT 名称或 BT 地址即可更改。

Audio Source BT MAC Name

  • 清除配对信息

用于清除配和耳机的配对绑定信息,清除后需要进入配对重新连接。

  • 开启/关闭BT配对

用于开启/关闭BT配对,开启配对模式。

  • 开启/关闭BLE自动回连

存在配对信息情况下可以根据配对信息自动回连,关闭自动回连无法自动连接。

音乐通话体验:

当连接BT或BLE音频设备成功后,可以进行音乐播放、通话、通话录音等操作。电脑音频会通过UAC,然后经由蓝牙与耳机设备进行传输。

(5) UI使用说明

按键 短按 双击 三击
key1(SW2/SW24) 音乐播放/暂停 下一曲 开启配对
key2(SW4/SW23) 上一曲 BT/BLE audio切换
key3(SW3/SW20) 音量+(BT)
key4(SW5/SW21) 音量-(BT)
SOURCE LED 闪烁 呼吸 常亮
LED1(白色) 未连接状态 连接状态
LED2(红色) 未连接状态 连接状态

A2DP In BIS Out

概述

本章节主要对Bluetooth Audio SDK中A2DP_TO_BIS参考设计进行简要介绍,方便用户理解及进行二次开发。该应用的主要功能为将BT传输过来的A2DP音乐包进行本地播放,并转换为Auracast广播包发送出去,多个接收端可同时接收该广播包,并与source端同步播放。

根据功能的不同,该应用可分为以下三个角色:

(1)Source:将接收到的A2DP音乐包解码为PCM数据进行本地播放,同步将PCM数据编码为Auracast广播包广播出去;

(2)Sync:切换完成后,自动开始扫描当前Demo广播的Broadcast Source广播,并与Source端同步播放;当Source丢失后,Synd端不会自动开始同步。

(3)Sink:切换完成后,设备被开始广播BIS-Sink的链接,用户可以用支持Auracast Assistant的设备配对并连接,使用BASS协议同步想要的Source,并完成同步。具体的可以参考ble example的Broadcast Sink Demo。

主要特性如下

  • BT相关特性

    • 支持BT链路配对,回连,角色切换等功能
    • 支持安全加密(SSP),自适应跳频(AFH)
    • 支持SNIFF模式
    • BT音乐支持SBC解码格式
    • 支持BT A2DP-SINK,AVRCP,SPP,GATT协议
  • BIS相关特性

    • 支持LC3编码
    • 支持BASS协议
    • 支持PBP协议
  • common特性

    • 支持WFI模式
    • 支持ASRC采样率转换及PPM多设备时钟偏差调节

应用接口

BT

  • 参考btble headset章节BT Host部分

BIS

  • 参考ble_example章节Broadcast Demo

UI使用说明

按键 单击 双击 三击
key1(SW2/SW24) 音乐播放/暂停 下一曲 开启配对
key2(SW4/SW23) BIS角色切换
key3(SW3/SW20) 音量+(BT) 音量-(BT)
SOURCE LED 闪烁 呼吸 常亮
LED1(白色) 未连接状态 连接状态
LED2(红色) 未连接状态 连接状态

BT/TPSLL TWS

概述

本文主要对Bluetooth Audio SDK中的BT/TPSLL TWS参考设计以及与之配合使用的TPSLL Audio Dongle的参考设计的实现方式进行阐述,对用户二次开发关注的模块进行详细说明,以期降低用户对于该参考设计的理解复杂度,缩短用户从立项到量产的整个开发周期。

BT/TPSLL TWS参考设计的典型应用场景包括:

(1)经典蓝牙的TWS(True Wireless Stereo)模式:通过经典蓝牙和手机或者PC建立连接,从而实现音乐播放或者拨打电话,即传统TWS耳机的功能。

(2)2.4G低延时音频模式(配合2.4G dongle使用):通过2.4G链路( Telink Proprietary Synchronous Link Layer,简称TPSLL)和2.4G dongle(dongle端使用TPSLL Audio Dongle参考设计)建立链接,2.4G dongle那一端可以插在PC,手机,以及平板电脑上实现低延时音频场景的应用,常用于对延时要求比较高的游戏场景,并且具备很好的跨终端的特性,此外,链接稳定性,抗干扰性能,距离表现,功耗方面,音频编解码等等,相比于经典蓝牙都有明显提升。

(3)经典蓝牙的 TWS和2.4G低延时音频同时在线的混音模式:在经典蓝牙TWS工作的同时,可以随时接入2.4G 音频;或者在2.4G低延时音频工作的同时,可以随时接入经典蓝牙的TWS,最终会形成多设备音源输入和混音的效果;这种设计很好地解决了无线链接场景的冲突问题,同时可以监听多种设备的音源输入,在不降低音频质量和延时的情况下,给用户带来了极致的体验。

BT/TPSLL TWS 产品形态示意图

主要特性如下

  • BT相关特性

    • 支持BT链路配对,回连,角色切换等功能
    • 支持安全加密(SSP),自适应跳频(AFH)
    • 支持SNIFF模式
    • 支持WFI和Suspend模式
    • BT音乐支持支持AAC/SBC编解码格式
    • BT通话支持支持CVSD和MSBC编解码格式
    • 支持BT HFP-HF,A2DP-SINK,AVRCP,SPP,GATT协议
    • 支持BT SPP/GATT的无线升级
    • 支持音频EQ,音乐模式支持9阶,通话模式上行支持4阶,通话下行支持4阶
    • 支持BT通话丢包补偿机制(PLC)
    • 支持MIC自动增益控制(AGC)
    • 支持动态调整音频信号范围(DRC)
    • 支持自动回声消除功能(AEC)
    • 支持噪声抑制功能(NS)
    • 支持BF波束成形工功能(BF Beamforming)
    • 支持BT通话上行的NN降噪
  • TPSLL相关特性

    • 支持TPSLL链路配对,回连
    • TPSLL Dongle音乐支持单/双耳模式,支持LC3 PLUS 48K 24bit
    • TPSLL Dongle电话支持单/双耳模式,支持下行 LC3 PLUS 48K 24bit,上行16K 24bit
    • 支持低延时模式28ms左右和超低延时模式18ms左右
    • 支持自适应跳频
    • 支持sniff模式
    • 支持WFI和suspend模式
  • BT和TPSLL共存特性

    • BT的任意场景均可和TPSLL任意场景共存
    • 电话共存时默认只有一路MIC上行,具体哪一路可选
  • TWS相关特性

    • 支持单双耳模式
    • 支持TWS双耳无线组队
    • 支持双耳之间的主从切换
    • 支持提示音(ADPCM格式)混音播放和单独播放功能
    • 支持Hybird ANC模式

UI使用说明:

(1) 按键说明

  • 耳机端
按键 短按 双击 三击 保持 长按(1~2s)
key1(SW24) 音乐播放/暂停 音量- 存盘
key2(SW23) 接通电话 上一曲 音量+
key3(SW20) 3s配对 主从切换 下一曲 siri
key4(SW21) 10s配对/开机 挂断电话 关机(会存盘)
  • Dongle端
按键 短按 双击 三击 保持 长按(1~2s)
key1(SW4) 配对

(2) LED指示说明

  • 耳机端
TWS LED 快闪 慢闪 呼吸 常亮
LED1(白色) 配对状态 回连状态 连接状态
LED2(红色) 配对状态 回连状态 连接状态

(3) Dongle端

Dongle LED 快闪 慢闪 呼吸 常亮
LED1(蓝色) 配对状态 回连状态 连接状态
LED2(红色) 配对状态 回连状态 连接状态

操作说明:

(1) 编译及烧录说明

BT/TPSLL TWS 参考设计与 TPSLL Audio Dongle 参考设计的编译及烧录过程可参考 Get Started 章节。以下只列出注意事项。

  • BT/TPSLL TWS参考设计
    • BT/TPSLL TWS 所用的 TL751x 为双核设计,导入 TL751x 工程后,需要先编译 controller 工程,再编译 bttpsll_tws 工程。编译 controller 时,需要在 vendor\controller\controller_config.h 文件中,将 CONTROLLER_MODE 设置为 BT/TPSLL_TWS。
#define CONTROLLER_MODE           BTTPSLL_TWS

bttpsll_tws 工程编译完成后会生成两个 bin 文件,存放在
tl_bluetooth_audio_sdk\telink_b91m_bluetooth_src\tlk_bluetooth_src\build\TL751X\bttpsll_tws 目录下,文件名分别为 bttpsll_tws&n22_controller_120.bin / bttpsll_tws&n22_controller_121.bin。两个 bin 文件需要分别通过 BDT 工具烧录到 TWS 左右耳的零地址。此外,提示音固件需要烧录到 0x001A0000 地址, DSP 固件烧录到 0x00200000 地址。

  • TPSLL Audio Dongle 参考设计

    • TPSLL Audio Dongle 所使用的 TL721x 为单核设计,导入 TL721x 工程后只需要编译 tpsll_audio_dongle 即可。编译成功后在: tl_bluetooth_audio_sdk\telink_b91m_bluetooth_src\tlk_bluetooth_src\build\TL721X\tpsll_audio_dongle

目录下会生成 tpsll_audio_dongle.bin,可通过 BDT 烧录到 TL721x 开发板中。

(2) 配置说明

BT/TPSLL TWS想要正常运行,在烧录完必要的固件后,还需要进行如下配置。

  • BT/TPSLL TWS 配置
    • USB ID 配置
      • 耳机端需要将 TWS 的 USB ID 保存在 flash 地址 '0x7f8000',左右耳需要分别写入 0x20,0x21。
    • BT 配置
      • 可通过上位机配置 BT 名称及 MAC 地址。

BT模式相关配置集成在上位机工具中(TelinkBluetoothTool),以下是上位机工具使用说明:

上位机工具

上位机通过串口与开发板进行通信,使用前需要使用串口工具连接 TL751x 开发板与电脑(耳机的PC6 PB7分别接到串口工具的RX TX),波特率配置为1,500,000。在上位机工具中选择串口并点击“打开串口”,建立上位机与开发板之间的通信。如需设置BT名称与地址,可以通过点击“获取BT名称”、“设置BT名称”完成。

  • TPSLL Audio Dongle 配置

    • Dongle 端需要在 flash 地址 '0x1ff100'位置写入一个自定义的 6 bytes Mac地址,注意不能为全 0 或全 f。

以上配置完成后,可以正常进行双耳组队以及 BT、TPSLL 连接。

(3)双耳组队操作说明

TWS双耳组队流程可以通过按键触发。双耳上电后分别单击 key4 开始组队,主耳与从耳分别出现如下 log,说明已经完成耳机组队:

  • TPT_HEADSET_STATE_CONNECTED:TPT_HEADSET_ROLE_MASTER
  • TPT_HEADSET_STATE_CONNECTED:TPT_HEADSET_ROLE_SLAVE

(4)BT连接操作说明

TWS 双耳单击 Key4, 组队成功后开启scan,此时手机打开蓝牙,可以根据BT名称点击对应设备进行连接。

连接完成之后,TWS 双耳 LED 状态由快闪变为同频呼吸。此时手机播放音乐或拨打电话,可以在耳机端接入耳机听到(TWS 左右耳都需要将耳机接到左声道)。

(5)TPSLL连接操作说明

Dongle 通过 USB 接入电脑,可在扬声器设置里看到名为“TLSR-BTBLE-MIC-SPK”的设备。

TWS 双耳单击 key4, 双耳组队完成后进入配对模式,此时 dongle 双击 key1 可与 TWS 建立连接。

连接完成后,TWS 双耳 LED 状态由快闪变为同频呼吸,dongle LED 状态从快闪变为常亮。 此时从电脑端播放音乐或拨打电话,音频输出设备选择 TLSR-BTBLE-MIC-SPK,可以建立完整的音频通路。

系统架构

(1) 耳机端软件架构

耳机端的软件框架如下图所示,主要包括:应用层(Application),操作系统(RTOS),协议层(Profiles/GAP/SDP/SMP/L2CAP等),音频通路(Audio Path),HCI(Host和Controller之间的接口层),控制器(BT controller/TPSLL controller),物理层和射频传输层(RF/PHY),电源管理(power manager)等。

BT/TPSLL TWS 耳机端软件框图

基于BT/TPSLL TWS参考设计,本文档主要对用户二次开发用到的内容进行详尽描述,对部分原理进行简要概括,对于Bluetooth 标准协议部分,以及核心库的实现不会提及。主要内容如下:

  • TWS双耳组队,配对,回连,单双耳模式切换
  • TWS双耳无缝切换
  • TWS耳机端的音频通路

(2) dongle端软件架构

dongle端的软件框架如下图所示,主要包括:应用层(Application),应用接口层(Application interface),音频通路(Audio Path),HCI(Host和controller之间的接口层),控制器(TPSLL controller),物理层和射频传输层(RF/PHY),电源管理(power manager)等

BT/TPSLL TWS dongle端软件框图

基于TPSLL Audio dongle参考设计,本文档主要对用户二次开发用到的内容进行详尽描述,对部分原理进行简要概括,对于核心库的实现不会提及。主要内容如下:

  • dongle和TWS耳机配对,回连
  • dongle端的音频通路

软件模块

(1) 耳机端软件模块介绍

TWS耳机双耳组队,配对,回连,单双耳模式切换:

1) 组队模式

在讲解10s组队和3s组队之前,我们先了解一个概念:组队模式。在我们预设的应用场景中,我们的参考设计可以同时覆盖两种模式,分别如下:

  • 有线组队:通常用于TWS耳机(带耳机仓);

    • 针对TWS耳机而言,由于有耳机仓的存在,耳机和仓之间可以通信,所以耳机仓可以主动统一两只耳机之间的组队信息,比如我们这里是通过耳机仓将左耳和右耳的MAC地址进行了交换,再按照固定规则生成统一的组队信息,让两只耳机均以私密的相同的组队信息进行组队,这样安全性更高;以下在讲到10s组队和3s组队的时候,默认都是使用有线组队模式;
  • 无线组队:通常用于TWS音响;

    • 针对TWS音响而言,由于没有耳机仓的存在,所以无法进行通信,统一组队信息;这个时候,我们依赖左右耳机之间使用公共地址和信道进行组队;组队成功后就会交换MAC地址,之后再组队时就会以相对安全的方式进行组队了;相对于有线组队而言,无线组队的方式在首次组队时由于使用了公共地址和信道所以更容易受到干扰。

2) 主副耳角色确定

根据先发送的组队数据包被对方收到的作为主耳,即A发送的组队数据包,B先收到了,那么A就作为主耳,B就为副耳;

  • 作为主耳,拥有整个链路的控制权,包括:拥有完整的BT链路,拥有完整的dongle链路,以及各类链路请求的决策权,响应各类链路请求,响应各类信息同步请求,以及拥有上行MIC的控制权等等。
  • 作为副耳,拥有链路的监听权,无控制权,包括:虚拟dongle链路,虚拟BT链路,发起各类链路请求,发起各类信息同步请求等等。

3) 单双耳模式切换

实际TWS耳机或者TWS音响使用过程中,会出现仅仅使用一个耳机或者一个音箱的情形,这就出现了我们所谓的单耳模式,相对而言,两个耳机或者音箱同时工作就是双耳模式;

  • 单耳模式:如果在组队期间,超时未成功组队,那么就会假定只有一只TWS耳机出仓,或者只有一个TWS音箱打开,这个时候就会进入单耳模式;单耳模式下,耳机可以正常和手机建立BT连接,和dongle建立TPSLL 连接,以及各类音频播放场景;此外,单耳模式下要允许随时可以和另外一只耳机组队上,实现单耳模式到双耳模式的切换;并且,单耳模式的耳机在切换到双耳模式时一定会变成主耳,后连接上的耳机或者音箱一定会变成副耳(从耳);
  • 双耳模式:如果在组队期间,成功组队上,那么就会进入双耳模式;双耳模式下,耳机可以正常和手机建立BT连接,和dongle建立TPSLL连接,以及各类音频播放场景;此外,双耳模式下支持无缝切换,从角色变换角度,我们也称为主从(副)切换;实际使用场景下,我们经常会在双耳模式下,将一个耳机放入仓内,或者关闭一个音箱,这个时候就会出现双耳模式到单耳模式的切换;但是,当关闭的这个耳机或者音箱,之前是主耳时,那么就会先触发主从切换,保证工作的这只耳机或音箱一定是主耳,这个和前面提到的单耳模式下的耳机或音箱未来一定做主耳的逻辑保持一致;

4) 双耳10s组队和配对

TWS耳机双耳10s组队过程如下图所示

BT/TPSLL TWS 耳机双耳10s组队流程图

在BT/TPSLL TWS参考设计中,TPSLL链路充当着重要的角色,双耳之间通过TPSLL链路进行组队,同步信息,虚拟BT链路等等;双耳与dongle建立音频链路也是通过TPSLL链路实现的;BT部分有标准的协议支持,使用接口有专门的文档描述,这里不再赘述;所以,本文档主要会针对TPSLL链路的上层应用接口以及用户需要知道的部分BT接口进行详细描述;

按照上述流程,依次介绍各个部分的实现原理和用到函数接口如下:

  • 初始化:整个Bluetooth Audio SDK都是采用了模块化设计,上层application在调用底层组件时,会先调用相关的模块初始化;
static void tlkapp_host_init(void)
{
    tlkmw_host_init();  
    tlksys_task_regEvtCB(TLKSYS_TASKID_HOST,TLKSYS_TASK_EVT_HOST_HCI,tlkapp_host_hci_handler);
#if (TLK_STK_BT_ENABLE)
    tlkapp_host_addModule(tlkapp_host_bt_getModule());
#endif
#if (TLK_STK_BLE_ENABLE)
    tlkapp_host_addModule(tlkapp_host_le_getModule());
#endif
#if (TLK_STK_BT_TPSLL_ENABLE)
    tlkapp_host_addModule(tlkapp_host_tph_getModule());
#endif
#if (TLKSTK_BTTPSLL_TWS_ENABLE)
    tlkapp_host_addModule(tlkapp_host_tpt_getModule());
#endif
#if (TLK_STK_TPD_ENABLE)
    tlkapp_host_addModule(tlkapp_host_tpd_getModule());
#endif
#if (TLK_STK_TPMD_ENABLE)
    tlkapp_host_addModule(tlkapp_host_tpmd_getModule());
#endif
}

如上述代码所示,在 BTTPSLL TWS 参考设计中,只有 TLKSTK_BTTPSLL_TWS_ENABLE 宏会被使能,以此来完成 TWS 功能模块的函数注册;

TlkAppHostModule_t* tlkapp_host_tpt_getModule(void)
{
    static const TlkAppHostModuleCfg_t cfgs = {
        .hostType = TLKSYS_MSG_HOST_TYPE_TPT,
        .init     = tlkapp_host_tpt_init,
        .start    = tlkapp_host_tpt_start,
        .input    = tlkapp_host_tpt_msgHandle,
    };
    static TlkAppHostModule_t module = {
        .cfgs = &cfgs,
    };
    return &module;
}

如上述代码所示,TWS 功能模块的函数注册主要包括 3 个函数:

static void tlkapp_host_tpt_init(void)

作用:用于 TWS 上层应用接口的初始化,包括:功耗管理的初始化,状态机初始化,多核通信的初始化,TWS 角色初始化,以及组队参数的初始化等等。

static void tlkapp_host_tpt_start(void)

作用:初始化组队状态机,设置组队参数,启动定时器任务等等

int tlkapp_host_tpt_msgHandle(uint16 msgID, uint08 *pData, uint16 dataLen)

作用:消息处理句柄,根据不同的消息类型处理对应的事件,这些事件多半是由按键触发,不同的按键组合发送不同消息,触发不同的事件处理

int tlkapp_host_tpt_msgHandle(uint16 msgID, uint08 *pData, uint16 dataLen)
{
    (void) pData;
    (void) dataLen;
    switch(msgID){

        case TLKSYS_TPT_MSGID_3S_PAIR:
            tlkmdi_bt_tpt_pair_start_req(TPT_HOST_HEADSET_SETUP_MODE_3S,pData);
            break;
        case TLKSYS_TPT_MSGID_10S_PAIR:
            tlkmdi_bt_tpt_pair_start_req(TPT_HOST_HEADSET_SETUP_MODE_10S,pData);
            break;
        case TLKSYS_TPT_MSGID_ENTER_LOW_LATENCY_MODE:
            tlkmdi_bt_tpt_pair_start_req(TPT_HOST_HEADSET_SETUP_MODE_ULTRA_LOW_LATENCY,pData);
            break;           
        case TLKSYS_TPT_MSGID_START_HANDOVER:
            tlkmdi_bt_tpt_handover_start();
            break;
        case TLKSYS_TPT_MSGID_SEND_KEY:
            tlkapp_host_tpt_recvSendKeyDeal(pData,dataLen);
            break;
        case TLKSYS_TPT_MSGID_SHUT_DOWN:
            tlksys_pm_setChn(TLKSYS_PM_CHN_SYS,0,0);
            tlkmdi_bt_tpt_shut_down();
            break;
        default:
            return -TLK_ENOSUPPORT;
    }
    return TLK_ENONE;
}
  • 进入 10s 组队模式:单击 key4,会进入 10s 组队模式,通过按键扫描模块(关于这个部分的内容在 SDK 的公共单元里会有详细描述),获取 10s 组队的事件,在事件中调用回调函数
void tlkmdi_bt_tpt_pair_start_req(uint8_t isRefactory, uint08 *peerMac)

参数 1:isRefactory,表示链路建立模式,针对 10s 组队,这里的值为 TPT_HOST_HEADSET_SETUP_MODE_10S。本参考设计主要提供了如下链路建立模式

typedef enum
{
    TPT_HOST_HEADSET_SETUP_MODE_IDLE,
    TPT_HOST_HEADSET_SETUP_MODE_NORMAL,
    TPT_HOST_HEADSET_SETUP_MODE_3S,
    TPT_HOST_HEADSET_SETUP_MODE_10S,
    TPT_HOST_DONGLE_SETUP_MODE_NORMAL,
    TPT_HOST_DONGLE_SETUP_MODE_PAIRING,
    TPT_HOST_DONGLE_SETUP_MODE_CC_HEADSET,
    TPT_HOST_HEADSET_SETUP_MODE_ULTRA_LOW_LATENCY,
    TPT_HOST_HEADSET_SETUP_MODE_EXIT_ULTRA_LOW_LATENCY,
} tpt_headset_setup_mode_for_host_e;

参数 2:peerMac,表示需要通过耳机仓进行有线组队的对耳地址。此参数默认为 NULL,双耳通过无线方式进行组队。

  • 关闭链路和删除配对信息:在 10s 组对的事件回调函数里,我们会先判断是否有 BT 链路存在,这个链路包括:配对,回连,连接。如果有的话,会先尝试关闭,之后就会删除配对信息,删除配对信息是在如下子函数里完成的。
void tlkmdi_bt_tpt_pair_start(uint8_t isRefactory, uint08 *peerMac)
  • TPSLL 进入组队模式:在确认 BT 链路全部关闭之后,通过状态机流转,我们会尝试关闭 TPSLL 链路,等待 TPSLL 断连事件上报;这里涉及如下函数接口:
bool tlkmdi_bt_tpt_pair_procs(void)

除了需要和底层交互的事件外,整个链路建立的状态机流转基本都是在上述函数中处理的;

  • 等待 500ms 判断双耳是否组队上:在如下断连事件处理函数中,我们会切换状态机,并且初始化组队的参数,其中有个一参数就是超时 500ms 的时间设置,等待进行双耳组队;
static int tlkmdi_bt_tpt_headset_disconnect_CB(uint8_t *pData, uint16_t dataLen)

真正的双耳组队的启动操作,还是在 tlkmdi_bt_tpt_pair_procs 函数中根据状态机流转完成的;启动组队会涉及如下主要函数接口:

tpsll_hci_sendWriteHeadsetAccessCodeAndChnIDCmd(sTlkMdiBtTpsllTwsCtrl.ble_ac, sTlkMdiBtTpsllTwsCtrl.ble_ch);
tpsll_hci_sendHeadsetConnectSetupCmd(TPT_HOST_HEADSET_SETUP_MODE_10S, TLK_MDI_BT_TPT_SETUP_CONTROLLER_TIMEOUT_US);

组队成功的话,会有组队成功事件上报,在组队成功事件的回调函数里,我们会存储组队信息,并且会切换状态机,这里会涉及到如下函数接口

static int tlkmdi_bt_tpt_headset_connected_CB(uint8_t *pData, uint16_t dataLen)
  • 进入配对模式:然后在 tlkmdi_bt_tpt_pair_procs 函数中根据状态机流转,我们分别启动了 BT 配对和 dongle 配对;这里需要注意的是只有主耳或者单耳模式下的耳机才可以发起配对,副耳是不能发起配对的,所以这里会有一个角色检查并关闭 BT 链路的操作。涉及函数如下:
static void tlkmdi_bt_tpt_pair_enter(bool isSingle)
  • 等待连接:在 tlkmdi_bt_tpt_pair_procs 函数中,我们会反复确认,BT 或者 dongle 是否已经连接上,有任意一方连接上,就会复位链路建立的状态机。不管等待手机连接还是等待 dongle 连接,只要连接上,就会通过连接完成事件上报;应用层根据连接完成事件,完成状态机更新;对于副耳而言,一旦发现主耳连接上 BT 连接就会触发 BT 链路的虚拟,同样发现主耳连接上 dongle 也会触发 dongle 链路的虚拟;该过程全部是由是底层自动完成的,上层只在虚拟成功后收到链路连接完成事件;

至此,10s 组队流程结束,之后就是连接态的一些行为,播放音乐,拨打电话,不同场景下的混音等等。

5) 双耳 3s 组队和配对

TWS 耳机双耳 3s 组队过程如下图所示,这里注意红色标记的部分是和 10s 组队之间的明显区别。

BT/TPSLL TWS 耳机双耳3s组队流程图

按照上述流程,依次介绍各个部分的实现原理和用到函数接口如下:

  • 进入 3s 组队模式:单击 key3,会进入 3s 组队模式,和 10s 组队一样,3s 组队也通过按键扫描触发 3s 组队事件,在事件中调用回调函数
void tlkmdi_bt_tpt_pair_start_req(uint8_t isRefactory, uint08 *peerMac)

参数 1 表示链路建立模式,针对 3s 组队,这里的 isRefactory 值为:TPT_HOST_HEADSET_SETUP_MODE_3S

  • 关闭链路:在 10s 组队的事件回调函数里,我们会先尝试关闭 BT 链路,之后就会删除配对信息,但是 3s 组对不会删除配对信息,这个也是二者最大的区别。此设计的主要目的,是为了方便用户连接新的 dongle 或者新的手机,一旦连接上就会用新的配对信息覆盖旧的,完成设备配对信息的替换;如果没有连接上新的设备,在超时未成功连接,或者重新上电后都会以旧的配对信息触发回连操作;

  • TPSLL 进入组队模式:在确认 BT 链路全部关闭之后,通过状态机流转,我们会尝试关闭 TPSLL 链路,等待 TPSLL 断连事件上报;这里涉及如下函数接口:

bool tlkmdi_bt_tpt_pair_procs(void)

除了需要和底层交互的事件外,整个链路建立的状态机流转基本都是在上述函数中处理的;

  • 等待 500ms 判断双耳是否组队上:在如下断连事件处理函数中,我们会切换状态机,并且初始化组队的参数,其中有个一参数就是超时 500ms 的时间设置,等待进行双耳组队;
static int tlkmdi_bt_tpt_headset_disconnect_CB(uint8_t *pData, uint16_t dataLen)

真正的双耳组队的启动操作,还是在 tlkmdi_bt_tpt_pair_procs 函数中根据状态机流转完成的;启动组队会涉及如下主要函数接口:

tpsll_hci_sendWriteHeadsetAccessCodeAndChnIDCmd(sTlkMdiBtTpsllTwsCtrl.ble_ac, sTlkMdiBtTpsllTwsCtrl.ble_ch);
tpsll_hci_sendHeadsetConnectSetupCmd(TPT_HOST_HEADSET_SETUP_MODE_10S, TLK_MDI_BT_TPT_SETUP_CONTROLLER_TIMEOUT_US);

组队成功的话,会有组队成功事件上报,在组队成功事件的回调函数里,我们会存储组队信息,并且会切换状态机,这里会涉及到如下函数接口

static int tlkmdi_bt_tpt_headset_connected_CB(uint8_t *pData, uint16_t dataLen)
  • 进入配对模式:然后在 tlkmdi_bt_tpt_pair_procs 函数中根据状态机流转,我们分别启动了 BT 配对和 dongle 配对;这里需要注意的是只有主耳或者单耳模式下的耳机才可以发起配对,副耳是不能发起配对的,所以这里会有一个角色检查并关闭 BT 链路的操作。涉及函数如下:
static void tlkmdi_bt_tpt_pair_enter(bool isSingle)
  • 等待连接:在 tlkmdi_bt_tpt_pair_procs 函数中,我们会反复确认,BT 或者 dongle 是否已经连接上,有任意一方连接上,就会复位链路建立的状态机。不管等待手机连接还是等待 dongle 连接,只要连接上,就会通过连接完成事件上报;应用层根据连接完成事件,完成状态机更新;对于副耳而言,一旦发现主耳连接上 BT 连接就会触发 BT 链路的虚拟,同样发现主耳连接上 dongle 也会触发 dongle 链路的虚拟;该过程全部是由是底层自动完成的,上层只在虚拟成功后收到链路连接完成事件;

至此,3s 组队流程结束,之后就是连接态的一些行为,播放音乐,拨打电话,不同场景下的混音等等。

6) TWS 耳机回连

从实际应用场景来看,TWS 耳机回连需要分为如下 2 类:

  • 开机回连:属于正常行为,通常发生在耳机之前已经和手机连接过或者和 dongle 连接过,那么当关机后再次出仓时,就会触发回连操作。
  • 异常断开回连:属于异常行为,比如因为连接距离远了导致的超时断开,或者因为某些异常行为导致的断连操作等,都会触发异常断开回连。

如下图所示,为整个开机回连的过程:

BT/TPSLL TWS 耳机双耳回连流程图

实际应用场景中的异常断开回连,从设备关系来看主要分为:

  • 主耳和从耳之间的异常断连:主从耳之间因为未知异常或者超距导致断连,一旦断连,主耳会切换成单耳模式继续保持配对,回连,或者连接状态;从耳一旦和主耳断连会关闭所有链路,等待和主耳组队上之后再重新虚拟所有链路。需要注意的是:配对和回连只有主耳可以发起,从耳只会虚拟连接态的链路。
  • 耳机和 dongle 之间的异常断连:耳机和 dongle 之间因为未知异常或者超距导致断连后,主耳和 donge 都会发起回连,一旦主耳和 dongle 连接上,从耳就会虚拟出 dongle 链路,监听 dongle 的音频数据包;
  • 耳机和手机之间的异常断连:耳机和手机之间因为未知异常或者超距导致断连后,主耳和手机都会发起回连,一旦主耳和手机回连上,从耳就会虚拟出 BT 链路,监听手机的数据包;
  • 其他异常断连处理说明:从链路设计的可靠性角度,我们不允许从耳比主耳先和 dongle 断开,从耳永远是等待主耳通知之后才会和 dongle 断开。同理,我们也不允许从耳比主耳先和手机断开,从耳永远是等待主耳通知之后才会和手机断开。这种设计大大简化了链路设计的复杂度,提高了链路的稳定性。此外,对于 dongle 和手机而言是看不到从耳的存在的,从耳附属在主耳上,以监听的角色,监听来自 dongle 和手机端的数据包。

这里主要涉及到的函数接口如下:

static int tlkmdi_bt_tpt_headset_disconnect_CB(uint8_t *pData, uint16_t dataLen)

作用:在断连事件的回调函数里,根据状态机触发不同操作行为

int tpsll_hci_sendHeadsetConnectSetupCmd(uint08 mode, uint32 timeout)

作用:启动左右耳之间的回连操作

void tlkmdi_bt_tpt_dongle_reconStart(void)
作用:启动主耳或单耳和 dongle 之间的回连操作

TWS 双耳无缝切换:

在单双耳模式切换的那一小节,我们有简单提过无缝切换的内容,这里我们会详细说一下。

首先,无缝切换是从用户体验出发给予的命名;如果从底层链路的角色变换角度来看,该行为会引起左右耳之间的主从角色的交换,所以我们也称为主从切换。主从分别对应无线通信系统中的主设备和从设备,主设备通常拥有整个链路的控制权,从设备附属在主设备上,一个主设备通常可以连接多个从设备。此外,从设备的时序要保证和主设备同步,在 BTTPSLL TWS 参考设计中,副耳(从耳)和 dongle 都是作为主耳的从设备存在,时序要和主耳同步,并且只有主耳可以有上行 MIC 数据。主耳拥有的这些特征决定了一旦主耳入仓关闭,那么外部的副耳(从耳)就需要接替主耳继续担当主耳的角色进行工作,即无论何时通信系统中至少存在一个主耳,一旦主耳关闭,其他设备要能接替主耳的角色,保证整个链路通信的完整性。

根据实际使用场景来看,需要进行无缝切换的场景如下:

  • 当主耳入仓后,会触发无缝切换:这个比较常见,保证仓外从耳可以接替主耳,继续担当主耳的角色
  • 当主耳低电后,会触发无缝切换:这个是从双耳电量均衡的角度来考虑,因为主耳拥有链路控制权,加上只有主耳可以有 MIC 上行数据,整体功耗会比从耳要高,长时间工作后,电量会明显下降;我们可以添加自动检测机制,来均衡主从耳中间的电量消耗;
  • 当用户 UI 命令触发后,会触发无缝切换:通常用于调试模式,在调试模式下,便于进行问题定位和梳理,保证无缝切换功能的兼容性和稳定性。

上层应用主要会用到如下函数接口:

/**
 * @brief       This function initiates the handover process for TWS (True Wireless Stereo) devices.
 *              Depending on the device role (master or slave), it either starts the handover command
 *              or requests a handover from the master device.
 * @return      none.
 * @note        
 */
void tlkmdi_bt_tpt_handover_start(void)

作用:启动主从切换,对于主耳而言,是直接启动主从切换,对于从耳而言,会给主耳发送请求,由主耳发起主从切换

/**
 * @brief       Handle the extraction of host information during handover process
 *              This function sets the handover status to EXTRACER, copies the Bluetooth
 *              address from input data to control structure, and triggers a TPSLL event
 *              in the HOST task
 * @param[in]   pData: pointer to the data containing Bluetooth address information
 *                      expected to be 6 bytes long
 */
_attribute_ram_code_sec_ 
void tlkmdi_bt_tpt_handover_extraceHostInfoHandler(uint8_t *pData)

作用:主耳在真正切换角色之前,会把自己的HOST 参数同步给从耳,便于从耳虚拟出主耳的全部HOST行为

/**
 * @brief       Handle TWS handover success event
 * @param[in]   pData    - Pointer to the data containing handover information, 
 *                         with the first byte representing the new role
 * @param[in]   dataLen  - Length of the data in bytes
 * @return      TLK_ENONE - Operation completed successfully
 * @note        This function processes the handover success event, updates the device role,
 *              handles specific actions based on the new role (master/slave/single), 
 *              and manages reconnection of Bluetooth and dongle connections as needed.
 */
static int tlkmdi_bt_tpt_handover_success(uint8_t *pData, uint16_t dataLen)

作用:在主从切换成功后,会上报success事件,新主耳就会基于收到的参数更新HOST,担当起主耳的角色

(2) dongle端软件模块介绍

dongle端与TWS耳机配对,回连:

相比较于耳机端而言,dongle的行为模式比较简单,如下图所示为dongle的配对和回连流程:

BTTPSLL TWS dongle配对回连流程图

  • 初始化:如下图所示,在TPSLL Audio dongle的参考设计中,我们通过使能TLK_STK_TPD_ENABLE 宏,在tlkapp_host_init初始化函数中,注册了TPSLL Audio dongle的组件模块。
static void tlkapp_host_init(void)
{
    tlkmw_host_init();
    tlksys_task_regEvtCB(TLKSYS_TASKID_HOST,TLKSYS_TASK_EVT_HOST_HCI,tlkapp_host_hci_handler);
#if (TLK_STK_BT_ENABLE)
    tlkapp_host_addModule(tlkapp_host_bt_getModule());
#endif
#if (TLK_STK_BLE_ENABLE)
    tlkapp_host_addModule(tlkapp_host_le_getModule());
#endif
#if (TLK_STK_BT_TPSLL_ENABLE)
    tlkapp_host_addModule(tlkapp_host_tph_getModule());
#endif
#if (TLKSTK_BTTPSLL_TWS_ENABLE)
    tlkapp_host_addModule(tlkapp_host_tpt_getModule());
#endif
#if (TLK_STK_TPD_ENABLE)
    tlkapp_host_addModule(tlkapp_host_tpd_getModule());
#endif
#if (TLK_STK_TPMD_ENABLE)
    tlkapp_host_addModule(tlkapp_host_tpmd_getModule());
#endif
}

核心函数如下:

TlkAppHostModule_t* tlkapp_host_tpd_getModule(void)
{
    static const TlkAppHostModuleCfg_t cfgs = {
        .hostType = TLKSYS_MSG_HOST_TYPE_TPD,
        .init    = tlkapp_host_tpd_init,
        .start   = tlkapp_host_tpd_start,
        .input   = tlkapp_host_tpd_input,
    };
    static TlkAppHostModule_t module = {
        .cfgs = &cfgs,
    };
    return &module;
}

作用:注册TPSLL Audio dongle的组件

static void tlkapp_host_tpd_init(void)

作用:模块初始化,音频通路初始化等

static void tlkapp_host_tpd_start(void)

作用:启动上电回连或者配对操作

static int tlkapp_host_tpd_input(uint16_t msgID, uint8_t *pData, uint16_t dataLen)

作用:处理消息

  • dongle配对:在初次上电,或者按配对键都会触发进入配对模式,在配对模式下,dongle可以和新的耳机配对,并且更新保存的配对信息;之后再触发回连时,就可以用新的配对信息触发回连。涉及到的函数接口如下:
u8 tpd_host_dongle_start_connection_scan(void)
  • dongle回连:和耳机回连类似,dongle回连也分为两种情况:

1) 上电回连:在已经完成配对,保存了配对信息之后,如果重新上电的话就会触发该回连操作,回连流程如上图所示。涉及到的函数接口如下:

int tlkmdi_tpsll_audio_dongle_powerOnReconHeadset(void)
2) 异常断开回连:在配对信息保存的基础上,因为未知异常或者超距导致断连的时候,就会发生异常断开回连。涉及到的函数接口如下:

static void tlkmdi_tpsll_audio_dongle_headset_disconnected_handler(uint8_t disconnect_reason)

其他说明

  • dongle的配对和回连调用的是相同的函数接口:tpd_host_dongle_start_connection_scan,区别在于设置的连接参数不同。

  • 耳机主从切换期间,dongle端是无感的,不需要关注。

BT/TPSLL Headset

概述

本章节主要对 Bluetooth Audio SDK 中 BT/TPSLL Headset 参考设计以及与之配合使用的 TPSLL Audio Dongle 的参考设计的实现方式进行阐述,对用户二次开发关注的模块进行详细说明,以期降低用户对于该参考设计的理解复杂度,缩短用户从立项到量产的整个开发周期。

BT/TPSLL Headset 是 BT/TPSLL TWS 参考设计的简化版本。该设计从 BT/TPSLL TWS 双耳架构“单耳化“精炼而来,在保留核心音质、低延迟等关键性能指标的同时,裁剪掉从耳同步链路、充电仓双向通信等模块,使整个设计更加简单和轻量化。

BT/TPSLL Headset 参考设计的典型应用场景包括:

  • 经典蓝牙的头戴式耳机模式:通过经典蓝牙和手机或者 PC 建立连接,从而实现音乐播放或者拨打电话,即传统 Headset 耳机的功能。

  • 2.4 G 低延时音频模式(配合2.4 G dongle 使用):通过 2.4 G 链路( Telink Proprietary Synchronous Link Layer,简称 TPSLL)和 2.4 G dongle(dongle 端使用 TPSLL Audio Dongle 参考设计)建立链接。2.4 G dongle 一端可以插在 PC、手机以及平板电脑上实现低延时音频场景的应用;常用于对延时要求比较高的游戏场景,并且具备很好的跨终端的特性。此外,链接稳定性、抗干扰性能、距离表现、功耗方面、音频编解码等等,相比于经典蓝牙都有明显提升。

  • 经典蓝牙和 2.4 G 低延时音频同时在线的混音模式:在经典蓝牙工作的同时,可以随时接入 2.4 G 音频;或者在 2.4 G 低延时音频工作的同时,可以随时接入经典蓝牙,最终会形成多设备音源输入和混音的效果;这种设计很好地解决了无线链接场景的冲突问题,同时可以监听多种设备的音源输入,在不降低音频质量和延时的情况下,给用户带来了极致的体验。

Product Diagram

主要特性如下

  • BT 相关特性

    • 支持 BT 链路配对、回连等功能
    • 支持安全加密(SSP),自适应跳频(AFH)
    • 支持 SNIFF 模式
    • 支持 WFI 和 Suspend 模式
    • BT 音乐支持 SBC 解码格式
    • BT 通话支持支持 CVSD 和 MSBC 编解码格式
    • 支持 BT HFP-HF、A2DP-SINK、AVRCP、SPP、GATT 协议
    • 支持 BT SPP/GATT 的无线升级
    • 支持音频 EQ,音乐模式支持9阶,通话模式上行支持4阶,通话下行支持4阶
    • 支持 BT 通话丢包补偿机制(PLC)
    • 支持动态调整音频信号范围(DRC)
    • 支持自动回声消除功能(AEC)
    • 支持噪声抑制功能(NS)
    • 支持 BF 波束成形工功能(BF Beamforming)
    • 支持 BT 通话上行的 NN 降噪
  • TPSLL 相关特性

    • 支持TPSLL链路配对、回连
    • TPSLL Dongle 音乐支持 LC3 PLUS 48K 24bit
    • TPSLL Dongle 电话支持下行 LC3 PLUS 48K 24bit,上行16K 24bit
    • 支持低延时模式 28 ms 左右
    • 支持自适应跳频
    • 支持 sniff 模式
    • 支持 WFI 和 suspend 模式
  • BT 和 TPSLL 共存特性

    • BT 的任意场景均可和 TPSLL 任意场景共存
    • 电话共存时默认只有一路 MIC 上行(默认 BT),具体哪一路可选

目录结构

bttpsll_headset
├── app_bt.c    // BT ACL,profile connection, disconnection event and reconnection processing module
├── app.c       // usb debug interface,tlkusb_debug_shell_hook()
├── app_config.h    // Engineering configuration file
├── app_key_led_config.c    // Key configuration file
└── main.c  // Project entrance

系统架构

(1) 耳机端软件架构

耳机端的软件框架如下图所示,主要包括:应用层(Application),操作系统(RTOS),协议层(SDP/A2DP/HFP/AVRCP等),音频通路(Audio Path),HCI(Host和Controller之间的接口层),控制器(BT controller/TPSLL controller),物理层和射频传输层(RF/PHY),电源管理(power manager)等。

Headset Software Framework Diagram

基于 BT/TPSLL Headset 参考设计,本文档主要对用户二次开发用到的内容进行详尽描述,对部分原理进行简要概括,对于 Bluetooth 标准协议部分,以及核心库的实现不会提及。主要内容如下:

  • Headset 配对、回连
  • Headset 端的音频通路

(2) dongle 端软件架构

dongle 端的软件框架如下图所示,主要包括:应用层(Application),应用接口层(Application interface),音频通路(Audio Path),HCI(Host和controller之间的接口层),控制器(TPSLL controller),物理层和射频传输层(RF/PHY),电源管理(power manager)等。

Dongle Software Framework Diagram

基于 TPSLL Audio dongle 参考设计,本文档主要对用户二次开发用到的内容进行详尽描述,对部分原理进行简要概括,对于核心库的实现不会提及。主要内容如下:

  • dongle 和 headset 配对、回连
  • dongle 端的音频通路

软件模块

(1) 耳机端软件模块介绍

​在 BTTPSLL Headset 参考设计中,TPSLL 链路同样充当着重要的角色,通过 TPSLL 链路进行配对、回连、音频数据传输等。耳机与 dongle 建立音频链路也是通过 TPSLL 链路实现的;BT 部分有标准的协议支持,使用接口有专门的文档描述,这里不再赘述;这一小节主要会针对 TPSLL 链路的上层应用接口以及用户需要知道的部分 BT 接口进行详细描述。

耳机配对、回连:

回连

回连流程于耳机上电后启动,配对期间的相关回连动作此处不再赘述。耳机上电后从 Flash 中读取 BT 和 dongle 的有效配对信息,如果配对信息存在,开始分别回连 BT 和 dongle;如果 BT 的有效配对信息不存在,默认开启 BT Page/Inquiry Scan 持续 120 s,如果 dongle 的有效配对信息不存在,耳机即进入 IDLE 态,直至用户手动关机或再次触发配对。

Reconnection Flowchart

配对

​与 BT/TPSLL TWS 一样,Headset 也有 10 s 配对和 3 s 配对的概念。在当前设计中,3 s 配对和 10 s 配对最大的区别为是否清除配对信息。

  • 3 s 配对

    • 3 s 配对期间,耳机以固定 Access Code 与 Channel ID 向周边 dongle 发起寻呼,并同步开启 BT Page/Inquiry Scan。若某 dongle 恰好处于配对模式且使用相同的 Access Code 与 Channel ID,双方将在交互窗口内完成快速识别与绑定。若 BT 先于 dongle 建立连接,耳机立即终止配对流程,切入 dongle 回连模式,尝试重新连接最近一次断开的 dongle;若 dongle 先于 BT 建立连接,耳机则重置 BT Page/Inquiry Scan 的剩余时间,并继续等待至 TLK_MDI_TPSLL_TPH_PAIRING_TIMEOUT(SDK 默认 30 s)。超时仍未配对成功,耳机自动转入回连模式,持续尝试重连上一次断开的 Dongle;若回连失败,则维持该状态直至用户手动关机或再次触发配对。
  • 10 s 配对

    • 10 s 配对首先擦除 Flash 中存储的 BT 及 dongle 配对信息,随后以固定的 Access Code 与 Channel ID 向周边 dongle 发起寻呼;后续流程与 3 s 配对相同。若超时仍未配对成功,耳机即进入 IDLE 态,直至用户手动关机或再次触发配对。
// Both 3s and 10s paring use the following Access Code and Channel ID
#define TPH_HOST_DONGLE_SETUP_COMMON_ACCESSCODE  0x56291435 
#define TPH_HOST_DONGLE_SETUP_COMMON_CHN         0x12 

Pairing Flowchart

按照上述流程,依次介绍各个部分的实现原理和用到函数接口如下:

  • 初始化:整个 Bluetooth Audio SDK 都是采用了模块化设计,上层 application 在调用底层组件时,会先调用相关的模块初始化;
static void tlkapp_host_init(void)
{
    tlkmw_host_init();  
    tlksys_task_regEvtCB(TLKSYS_TASKID_HOST,TLKSYS_TASK_EVT_HOST_HCI,tlkapp_host_hci_handler);
#if (TLK_STK_BT_ENABLE)
    tlkapp_host_addModule(tlkapp_host_bt_getModule());
#endif
        ...
#if (TLK_STK_BT_TPSLL_ENABLE)
    tlkapp_host_addModule(tlkapp_host_tph_getModule());
#endif
#if (TLK_STK_TPD_ENABLE)
    tlkapp_host_addModule(tlkapp_host_tpd_getModule());
#endif
        ...
}

如上述代码所示,在 BT/TPSLL Headset 参考设计中,通过使能 TLK_STK_BT_TPSLL_ENABLE 宏来完成 headset 功能模块的函数注册。

TlkAppHostModule_t* tlkapp_host_tph_getModule(void)
{
    static const TlkAppHostModuleCfg_t cfgs = {
        .hostType = TLKSYS_MSG_HOST_TYPE_TPH,
        .init    = tlkapp_host_tph_init,
        .handler = tlkapp_host_tph_handler,
        .input   = tlkapp_host_tph_msgHandle,
    };
    static TlkAppHostModule_t module = {
        .cfgs = &cfgs,
    };
    return &module;
}

如上述代码所示,headset 功能模块的函数注册主要包括 hostType 标识和 3 个函数:

hostType = TLKSYS_MSG_HOST_TYPE_TPH

作用:用于向 host 线程声明本模块的身份编码(TLKSYS_MSG_HOST_TYPE_TPH)。系统的所有线程(host、system、audio、lemger、user)通过该编码进行消息路由,接收方仅处理与自身 hostType 匹配的事件包,通过 tlkapp_host_tph_msgHandle() 处理,相当于模块在全局消息总线上的“地址”,保证 TPH 组件的初始化、运行与通信不被其他业务干扰。

static void tlkapp_host_tph_init(void)

作用:用于 headset 上层应用接口的初始化,包括状态机初始化、多核通信的初始化、以及组队参数的初始化等。

static void tlkapp_host_tph_handler(void)

作用:状态机驱动,主要用于流转 headset 配对、回连的状态。

int tlkapp_host_tph_msgHandle(uint16 msgID, uint08 *pData, uint16 dataLen)

作用:消息处理函数,与 hostType 强绑定。根据不同的消息类型处理对应的事件,这些事件多半是由按键触发,不同的按键组合发送不同消息,触发不同的事件处理

int tlkapp_host_tph_msgHandle(uint16_t msgID, uint8_t *pData, uint16_t dataLen)
{
    (void) msgID;
    (void) pData;
    (void) dataLen;
    switch(msgID){
        case TLKSYS_TPH_MSGID_3S_PAIR:
            tlkmdi_bt_tph_pair_start(false);
            break;
        case TLKSYS_TPH_MSGID_10S_PAIR:
            tlkmdi_bt_tph_pair_start(true);
            break;
        case TLKSYS_TPH_MSGID_SEND_KEY:
            return tlkapp_host_tph_recvSendKeyDeal(pData,dataLen);
        default:
            return -TLK_ENOSUPPORT;
    }
    return TLK_ENONE;
}
  • 进入配对模式:双击或三击配对按键,进入配对模式。通过按键扫描模块(关于这个部分的内容在 SDK 的公共单元里会有详细描述),将配对消息路由到该模块并调用下面的配对函数进入配对状态。
void tlkmdi_bt_tph_pair_start(bool isRefactory)

isRefactory 表征链路建立模式。true 表示 10 s 配对,false 表示 3 s 配对。本参考设计主要提供了如下链路建立模式,但耳机端的当前只用到了 TPH_HOST_DONGLE_SETUP_MODE_CC_HEADSET。

typedef enum
{
    TPH_HOST_HEADSET_SETUP_MODE_IDLE,
    TPH_HOST_HEADSET_SETUP_MODE_NORMAL,
    TPH_HOST_HEADSET_SETUP_MODE_3S,
    TPH_HOST_HEADSET_SETUP_MODE_10S,
    TPH_HOST_DONGLE_SETUP_MODE_NORMAL,
    TPH_HOST_DONGLE_SETUP_MODE_PAIRING,
    TPH_HOST_DONGLE_SETUP_MODE_CC_HEADSET,
} tph_headset_setup_mode_for_host_e;

一旦进入任一配对模式并成功与 dongle 建立连接,系统将先后触发 TPSLL_EVTID_DONGLE_MAC_UPDATE 与 TPSLL_EVTID_DONGLE_CONNECT 事件,其回调函数负责保存 dongle 配对信息、向 TPSLL 控制器写入对端 MAC 地址、更新 dongle 连接状态,并同步刷新提示音与 LED 指示等。

static int tlkmdi_bt_tph_dongle_macUpdateHandler(uint8_t *pData, uint16_t dataLen)
static int tlkmdi_bt_tph_dongle_connectHandler(uint8_t *pData, uint16_t dataLen)
  • 状态机流转(状态迁移):该状态机是 BT/TPSLL Headset 配对与回连流程的核心调度器,以 “运行到静止” 模型实现。tlkmdi_bt_tph_pairing_handler() 在每次系统 loop 中被调用,只要任意状态返回 true 就立即重入,直到无状态迁移为止,保证一次调度内完成所有可连续步骤。
int tlkmdi_bt_tph_pairing_handler(void)
  • 状态总览
状态宏 语义
TLKMDI_TPSLL_PAIRING_ASYNC_DISCONNECTED dongle 链路已经断开
TLKMDI_TPSLL_PAIRING_BT_DSICON_WAITING 等待 BT ACL 链路断开
TLKMDI_TPSLL_PAIRING_BT_DISCONNECTED BT ACL 链路已断开,进入可发现/配对状态
TLKMDI_TPSLL_PAIRING_CONNECT_WAITING 已开启扫描/寻呼,等待 dongle 连接
TLKMDI_TPSLL_IDLE 上电初始或回连失败后的空闲态
  • 运行原理

“单步原子”设计规定:任一 tlkmdi_bt_tph_nowPSM_xxx() 函数仅执行单一操作,下发 HCI 命令、更新状态字,随后返回 true 以请求立即重调度,返回 false 或当前状态不处在状态机的的调度范围内,立即停止状态机并进入 IDLE 状态,直到下次被触发。

实际应用场景中的异常断开回连,从设备关系来看主要分为:

  • 耳机和 dongle 之间的异常断连:耳机和 dongle 之间因为未知异常或者超距导致断连后,耳机和 donge 都会发起回连并保持回连状态直到回连成功或用户手动触发配对。
  • 耳机和手机之间的异常断连:耳机和手机之间因为未知异常或者超距导致断连后,耳机会发起回连。

这里主要涉及到的函数接口如下:

static int tlkmdi_bt_tph_dongle_disconnHandler(uint8_t *pData, uint16_t dataLen)

作用:在断连事件的回调函数里,主要刷新提示音与 LED 指示、根据状态机触发不同操作行为。

void tlkmdi_bt_tph_dongle_reconnStart(void)

作用:启动耳机回连 dongle 的动作。

(2) dongle端软件模块介绍

该模块在 BT/TPSLL TWS 中已经介绍过,原理和机制与 TWS 完全一致,不再赘述。

LE example Demo

概述

BLE example 工程全部统一的配置,模块化的管理方式,方便开发者快速验证,使用以及新建不同的单BLE应用示例。工程通过 "app_example.h"文件来实现不同应用的配置,以及实现用户层实现。

支持的硬件

目前工程支持以下硬件平台:

MCU Series Board Model
TL721x C1TXA104_V1.1(CODEC1-V2)
C1T315A20
C1T315A20_V2
TLSR952x C1T266A20_V1.3
TL751x C1T368A20_V1_1
TL322x C1T371A20_V1_1
C1T379A20_V1_0

目录结构

vendor/ble_example/
├── app_example.h                    # 工程管理
├── app_config.h                     # 工程通用配置
├── app_create_new_demo/             # 应用示例目录
│   ├── app_create_new_demo.c        # 应用示例源代码
│   ├── app_create_new_demo_cfg.h    # 应用特殊配置
│   └── README.md                    # 应用的说明文档
├── app_key.c                        # 按键处理函数
├── app_key.h
└── main.c                           # 工程入口

(1) main.c

main.c 是工程的入口,主要完成了以下工作:

  • 初始化系统,包括初始化系统的一些基础资源,如系统定时器,系统事件,系统任务等;

  • 启动系统,启动系统的主要任务,包括初始化蓝牙协议栈,启动蓝牙协议栈的主循环,启动系统的其他任务等;

  • 进入系统的主循环,在循环中不断处理系统的事件,包括系统定时器事件,系统事件等;

如下面代码中,通过APP_DEMO_SELECT宏定义的选择,进入不同的应用示例来实现不同的功能。

int INIT(APP_DEMO_SELECT)(void);
void tlkapp_host_le_init(void)
{
    ble_stack_init();
    INIT(APP_DEMO_SELECT)();
}

int START(APP_DEMO_SELECT)(void);
void tlkapp_host_le_start(void)
{
    START(APP_DEMO_SELECT)();
}

int main(void)
{
    tlksys_init();
    tlksys_start(tlkapp_create_allTasks);

#if (!TLK_CFG_RTOS_ENABLE)
    while (1) {
    #if (BLE_CONTROLLER_INITIAL_EN)
        tlksdk_main_loop();
    #endif
        tlksys_handler();
    }
#endif

    return 0;
}

(2) app_key.h and app_key.c

app_key.h 和 app_key.c 实现了按键处理函数,用户可以根据自己的需求注册按键处理函数,当按键被按下时,会调用相应的回调函数。

按键Key ID默认的定义如下:

Key ID Description Key ID Description
0 Key 1 click 4 Key 1 double click
1 key 2 click 5 Key 2 double click
2 Key 3 click 6 Key 3 double click
3 Key 4 click 7 Key 4 double click

用户可以根据需求修改按键的定义,也可以在app_key.c中实现按键的处理函数。

/**
 *    @brief  Register a callback function for a specific key.
 *
 *    @param[in] key_id    Key ID.
 *    @param[in] callback  Callback function to be called when the key is pressed.
 *
 *    @return void
 */
void app_key_register_callback(uint8_t key_id, void (*callback)(void));

(3) app_config.h

app_config.h 定义了工程的通用配置,包括系统的一些基础配置,蓝牙协议栈的一些基础配置,以及应用的一些基础配置。通常情况下,用户不需要修改该文件。

(4) app_example.h

app_example.h 为整个工程提供了通用的管理方式,用户可以根据自己的需求选择不同的应用示例,并且不同的应用示例配置一些独特的配置,实现不同应用不同配置。

下面截取了实现的核心代码,文件提供了三个宏定义,分别是 START、INIT 和 IS_DEMO_SELECTED,并且为每个应用示例提供了独特的宏定义,比如 APP_BLE_NEW_DEMO,APP_BLE_ADV,APP_BLE_ACL。

#define START(...)                      EXPAND(START_(__VA_ARGS__))
#define INIT(...)                       EXPAND(INIT_(__VA_ARGS__))
#define IS_DEMO_SELECTED(...)           (GET_DEMO_ID(APP_DEMO_SELECT) == GET_DEMO_ID(__VA_ARGS__))

// 
#define APP_BLE_NEW_DEMO                app_new_demo, 1
// Simple BLE demo
#define APP_BLE_ADV                     app_adv, 100
#define APP_BLE_ACL                     app_acl, 101

// develop can not commit this select to gitlab.
#define APP_DEMO_SELECT                 APP_BLE_NEW_DEMO
  • INIT: 初始化函数,该函数会在系统初始化时被调用,用户可以在该函数中实现一些初始化工作,主要是蓝牙协议栈一些差异配置。
  • START: 启动函数,该函数会在系统启动时被调用,用户可以在该函数中实现一些启动工作,主要是启动蓝牙控制器,启动应用示例等。
  • IS_DEMO_SELECTED: 判断是否选择了某个应用示例,该宏会根据 APP_DEMO_SELECT 和传入的应用示例宏,判断是否选择了该应用示例,主要为一些通用功能提供选择,例如 tlkusb_debug_shell_hook 属于 SDK 通用功能,可能在不同的应用示例中都需要,所以需要通过该宏来判断是否选择了该功能。
  • APP_BLE_NEW_DEMO: 应用示例宏,该宏定义了新应用示例,主要由示例的名称和 ID。其中 ID 在 IS_DEMO_SELECTED 会使用到,需要保证 ID 的唯一性。
  • APP_DEMO_SELECT: 选择的应用示例,该宏定义了当前选择的应用示例,在 main.c 中会根据该宏选择对应的应用示例。

其中,START、INIT 和 IS_DEMO_SELECTED 三个宏定义,是通过宏展开的方式实现的,如想要了解,自行阅读代码或者咨询 AI 即可。

#if __has_include(CFG_PATH(APP_DEMO_SELECT))
#include CFG_PATH(APP_DEMO_SELECT)
#endif

#define STRINGIFY_HELPER(x)             #x
#define STRINGIFY(x)                    STRINGIFY_HELPER(x)

#define START(...)                      EXPAND(START_(__VA_ARGS__))
#define INIT(...)                       EXPAND(INIT_(__VA_ARGS__))
#define IS_DEMO_SELECTED(...)           (GET_DEMO_ID(APP_DEMO_SELECT) == GET_DEMO_ID(__VA_ARGS__))

#define CAT(a, b)                       a##b
#define EXPAND(x)                       x
#define EVAL(x)                         EXPAND(x)
#define TOSTRING(x)                     STRINGIFY(x)
#define CFG_PATH_(x, id)                TOSTRING(EVAL(CAT(x/x,_cfg.h)))
#define CFG_PATH(...)                   EXPAND(CFG_PATH_(__VA_ARGS__))
#define INIT_(x, id)                    EVAL(CAT(x, _init))
#define START_(x, id)                   EVAL(CAT(x, _start))
#define GET_DEMO_ID_(x, id)             id
#define GET_DEMO_ID(...)                GET_DEMO_ID_(__VA_ARGS__)

(5) app_create_new_demo/

app_create_new_demo/ 目录下提供了一些应用示例,用户可以根据自己的需求新建不同的应用示例,并且可以根据自己的需求实现不同的功能。

注意

  • 如果文件夹需要有特殊的配置文件,文件夹的名称必要要和工程宏定义的名称相同,配置文件的名称必须要_cfg.h 结尾,实现逻辑参考 app_example.h 的实现。
#include "stack/ble/ble.h"

#include "../app_example.h"

int INIT(APP_BLE_NEW_DEMO)(void)
{
    tlk_printf("Hello, Telink BLE Demo initialized!");
    return 0;
}

void START(APP_BLE_NEW_DEMO)(void)
{
    tlk_printf("Hello, Telink BLE Demo started!");
}

必须要实现的两个函数分别是 INIT 和 START,INIT 函数会在系统初始化时被调用,START 函数会在系统启动时被调用。如 APP_BLE_NEW_DEMO 示例,实现了初始化和启动的打印信息。

如何新建应用示例

(1) 注册 Demo 到 app_example.h

1) 定义 Demo 宏

在文件中的 Demo 定义区域添加新 Demo 的宏定义:

#define APP_BLE_MY_DEMO    app_my_demo, 500

格式说明

  • APP_BLE_MY_DEMO:Demo 的宏名称(建议全部使用大写)
  • app_my_demo:Demo 的实际名称(与文件夹名一致)
  • 500:Demo 的唯一 ID(确保不与现有 Demo ID 冲突)

ID 分配规则

  • 通用 BLE Demo:100-199
  • LE Audio Demo:200-299
  • 特定客户 Demo:1000-1099

2) 选择APP_BLE_MY_DEMO

修改 APP_DEMO_SELECT 宏,指向新创建的 Demo:

#define APP_DEMO_SELECT    APP_BLE_MY_DEMO

(2) 新建 app_my_demo 文件夹

vendor/ble_example 目录下创建以 app_my_demo 名称命名的文件夹。

vendor/ble_example/app_my_demo/

注意

  • 文件夹名称建议使用小写字母和下划线。
  • 文件夹名称将作为 Demo 标识符使用。

(3) 新建文件

在新建的app_my_demo文件夹中创建以下文件:

主实现文件(必需)

  • 文件名:{demo_name}.c
  • 示例:app_my_demo.c
  • 用途:实现 Demo 的主要功能代码

配置文件(可选,如需要协议栈配置)

  • 文件名:{demo_name}_cfg.h
  • 示例:app_my_demo_cfg.h
  • 用途:配置协议栈相关参数

说明文档(可选)

  • 文件名:README.md
  • 用途:记录 Demo 的功能说明和使用方法

实现基本函数

在 Demo 的主实现文件中,必须实现以下两个函数:

#include "stack/ble/ble.h"
#include "../app_example.h"

// 初始化函数
int INIT(APP_BLE_MY_DEMO)(void)
{
    // 初始化相关资源
    tlk_printf("My Demo initialized!");
    return 0;  // 返回 0 表示初始化成功
}

// 启动函数
void START(APP_BLE_MY_DEMO)(void)
{
    // Demo 的主逻辑入口
    tlk_printf("My Demo started!");
    // 在这里启动 Demo ,如打开扫描,打开广播
}

函数说明

  • INIT() 函数:在系统启动时调用,用于初始化 Demo 所需的资源
    • 返回值:0 表示成功,非 0 表示失败
  • START() 函数:在初始化完成后调用,是 Demo 的主逻辑入口

宏展开机制

  • INIT(APP_BLE_MY_DEMO) 会自动展开为 app_my_demo_init()
  • START(APP_BLE_MY_DEMO) 会自动展开为 app_my_demo_start()

编译和运行

完成以上步骤后,即可编译和运行新创建的 APP_BLE_MY_DEMO:

(1)确保 app_example.h 中的 APP_DEMO_SELECT 指向新 Demo

(2)编译工程

(3)下载固件到设备

(4)运行并验证功能

现有参考Demo说明

工程提供了很多参考 Demo,方便用户快速验证和使用。下面是各 Demo 的功能说明:

BLE New Demo:

实现了一个空的 BLE 应用示例,可以验证平台和芯片的基本功能,类似于经典的hello world。

BLE ADV Demo:

实现了传统广播的功能,可以用来验证传统广播的效果。

BLE ACL Demo:

实现了 BLE ACL的功能,主从一体的 BLE 设备,可以与智能手机实现主从通信。

BLE ACL Peripheral Demo:

实现了 BLE ACL Peripheral 的功能,可以用来验证 BLE Peripheral 设备的功能。

BLE ACL Central Demo:

实现了 BLE ACL Central 的功能,可以用来验证 BLE Central 设备的功能。

BLE OTA Demo:

实现了 BLE OTA 的功能,可以用来验证 BLE OTA 升级的功能。

BLE SMP Demo:

实现了 BLE SMP 的功能,可以用来验证 BLE SMP 加密的功能,该Demo是基于BLE ACL的扩展,可以验证不同的SMP加密方式。

BLE Random Address Demo:

实现了 BLE Random Address 的功能,可以用来验证 BLE 静态随机地址,可解性随机地址和非可解析随机地址的功能。

BLE HID Device Demo:

实现了 BLE HID Device 的功能,集成了BLE Keyboard和BLE Mouse,可以用来验证 BLE HID 设备的功能。

BLE Scan Demo:

实现了 BLE Scan 的功能,包括传统扫描和扩展扫描,可以用来验证 BLE 扫描的功能。

BLE SPP Server Demo:

实现了 BLE SPP Server 的功能,可以用来验证 BLE SPP Server 的功能。

BLE SPP Client Demo:

实现了 BLE SPP Client 的功能,可以用来验证 BLE SPP Client 的功能。

BLE SSDP Demo:

实现了 BLE SSDP(simple service discovery protocol) 的功能,可以用来验证 BLE SSDP 的功能,该Demo和BLE SPP Client Demo功能一样,只是调用的接口不同。

BLE iOS ANCS Demo:

实现了 BLE iOS ANCS(Apple Notification Center Service) 的功能,应用中提供了判断是否为iOS设备的方式,以及可以订阅iOS的通知。

LE Audio Unicast Server Demo:

实现了 LE Audio Unicast Server 的功能,可以用来验证 LE Audio 单播的功能,可以与支持LE Audio的手机实现LE Audio音频播放。

LE Audio Unicast Client Demo:

实现了 LE Audio Unicast Client 的功能,可以用来验证 LE Audio 单播的功能,可以与LE Audio Unicast Server实现LE Audio音频播放,包括音乐播放,双向通话,单麦克风功能。

LE Audio Source Demo:

实现了 LE Audio Source 的功能,标准的LE Audio 单播Client,可以通过上位机与标准LE Audio设备实现音频播放,包括音乐播放,双向通话。

LE Audio Auracast(Broadcast) Source Demo:

实现了 LE Audio Auracast(Broadcast) Source 的功能,可以用来验证 LE Audio 广播的功能。

LE Audio Auracast(Broadcast) Sink Demo:

实现了 LE Audio Auracast(Broadcast) Sink 的功能,可以用来验证 LE Audio 广播接收的功能,可以与LE Audio Auracast(Broadcast) Source互测。

LE Audio Device Demo:

实现了 LE Audio Device 的功能,Demo包含了 Unicast Server和Auracast(Broadcast) Sink的功能。可以与支持LE Audio的手机实现所有的LE Audio功能。

LE Audio Path

LE Audio Path(以下简称LEA音频通路)是基于当前SDK的音频通路架构实现的,主要为了测试LE Audio功能是否正常工作,并提供参考实现。

LEA音频通路的设计目标主要是提供一种兼容性较强,功能相对完整,可扩展性强,可靠性高的音频通路架构。音频链路的延迟等音频指标不在通用设计目标之内,因此LEA音频通路的设计目标主要是满足音频应用的需求。LE Audio相关项目的开发,需要优化音频通路设计,满足音频应用的需求。

(1) 架构设计

LE Audio架构分成三个部分:

  • 音频通路任务:包括Unicast Client(tlkmdi_lea_uc.c)、Unicast Server(tlkmdi_lea_us.c)、Broadcast Source(tlkmdi_leg_bms.c)和Broadcast Sink(tlkmdi_leg_bmr.c)。

  • 通用音频通路:包括音频输入(Audio Input)和音频输出(Audio Output)两个音频链路的数据处理。

  • 音频驱动模块: 主要是对于音频驱动的封装,包括音频输入和音频输出。

LE Audio Path架构图

LE Audio的架构图如上图所示,Audio Common负责音频的输入输出处理,包括LC3编解码器初始化,音频数据的编解码,音频同步播放的处理。

Codec模块对于不同的音频输入输出方式,封装抽象统一的音频接口,例如UAC、Codec、A2DP In、Sine Wave等。

特定的音频任务模块,基于当前SDK的音频通路模式开发,目前支持了LE Audio典型的四种音频任务:Unicast Client、Unicast Server、Broadcast Source、Broadcast Sink。

(2) Codec 模块

Codec模块的逻辑框图如下图,主要将不同的音频输入实例化为统一的音频接口,并提供Codec打开、关闭、音频数据获取、音频数据发送等接口。

Codec模块逻辑框图

Codec 模块打开:

Codec模块的初始化接口,主要分为Input和Output两个部分,分别对应音频的输入和输出。

  • Input Stream 初始化:is_input_stream_init字段用于标识是否初始化了音频输入流,true表示初始化Input Stream,false表示未初始化。input_sample_rate表示音频输入的采样率,采样率直接采用LE Audio协议定义的参数。input_location表示音频输入的位置,目前仅支持左声道音频、右声道音频和立体声音频三种配置。

  • Output Stream 初始化:is_output_stream_init字段用于标识是否初始化了音频输出流,true表示初始化Output Stream,false表示未初始化。output_sample_rate表示音频输出的采样率,采样率直接采用LE Audio协议定义的参数。output_location表示音频输出的位置,目前仅支持左声道音频、右声道音频和立体声音频三种配置。

/**
 * @brief LE Audio Codec configuration structure.
 */
struct lea_codec_config {
    bool     is_input_stream_init;
    bool     is_output_stream_init;
    uint8_t  input_sample_rate;
    uint32_t input_location;
    uint8_t  output_sample_rate;
    uint32_t output_location;
};

/**
 * @brief       Initialize LE Audio codec stream.
 * @param[in]   config    - pointer to the codec configuration structure.
 * @return      none.
 */
void lea_codec_stream_init(struct lea_codec_config *config);

音频采样率的参数,目前仅支持 8kHz、16kHz、24kHz、32kHz、48kHz 这几种采样率,具体采样率的定义如下:

// Audio Frame Frequency (for codec parameter)
enum lea_select_sampling_freq {
    LEA_SELECT_SAMPLING_FREQ_MIN,  /** < Minimum value for audio sampling frequency selection */
    LEA_SELECT_SAMPLING_FREQ_8000_HZ = 1,  /** < 8000 Hz */
    LEA_SELECT_SAMPLING_FREQ_11025_HZ = 2,  /** < 11025 Hz */
    LEA_SELECT_SAMPLING_FREQ_16000_HZ = 3,  /** < 16000 Hz */
    LEA_SELECT_SAMPLING_FREQ_22050_HZ = 4,  /** < 22050 Hz */
    LEA_SELECT_SAMPLING_FREQ_24000_HZ = 5,  /** < 24000 Hz */
    LEA_SELECT_SAMPLING_FREQ_32000_HZ = 6,  /** < 32000 Hz */
    LEA_SELECT_SAMPLING_FREQ_44100_HZ = 7,  /** < 44100 Hz */
    LEA_SELECT_SAMPLING_FREQ_48000_HZ = 8,  /** < 48000 Hz */
    LEA_SELECT_SAMPLING_FREQ_88200_HZ = 9,  /** < 88200 Hz */
    LEA_SELECT_SAMPLING_FREQ_96000_HZ = 10, /** < 96000 Hz */
    LEA_SELECT_SAMPLING_FREQ_176400_HZ = 11, /** < 176400 Hz */
    LEA_SELECT_SAMPLING_FREQ_192000_HZ = 12, /** < 192000 Hz */
    LEA_SELECT_SAMPLING_FREQ_384000_HZ = 13, /** < 384000 Hz */
    LEA_SELECT_SAMPLING_FREQ_MAX,  /** < Maximum value for audio sampling frequency selection */
};

音频输出位置的参数,目前仅支持 LEA_LOCATION_FRONT_LEFT,LEA_LOCATION_FRONT_RIGHT 和 LEA_LOCATION_FRONT_LEFT | LEA_LOCATION_FRONT_RIGHT 三种配置,具体配置的定义如下:

/** < LE Audio Location Definitions */
enum lea_location_flag {
    LEA_LOCATION_NONE = 0x0000,
    LEA_LOCATION_FRONT_LEFT = 1U << 0,   /** < Front Left */
    LEA_LOCATION_FRONT_RIGHT = 1U << 1,   /** < Front Right */
    LEA_LOCATION_FRONT_CENTER = 1U << 2,   /** < Front Center */
    LEA_LOCATION_LOW_FREQUENCY_1 = 1U << 3,   /** < Low Frequency Effects 1 */
    LEA_LOCATION_BACK_LEFT = 1U << 4,   /** < Back Left */
    LEA_LOCATION_BACK_RIGHT = 1U << 5,   /** < Back Right */
    LEA_LOCATION_FRONT_LEFT_OF_CENTER = 1U << 6,  /** < Front Left of Center */
    LEA_LOCATION_FRONT_RIGHT_OF_CENTER = 1U << 7, /** < Front Right of Center */
    LEA_LOCATION_BACK_CENTER = 1U << 8,   /** < Back Center */
    LEA_LOCATION_LOW_FREQUENCY_2 = 1U << 9,   /** < Low Frequency Effects 2 */
    LEA_LOCATION_SIDE_LEFT = 1U << 10,  /** < Side Left */
    LEA_LOCATION_SIDE_RIGHT = 1U << 11,  /** < Side Right */
    LEA_LOCATION_TOP_FRONT_LEFT = 1U << 12,  /** < Top Front Left */
    LEA_LOCATION_TOP_FRONT_RIGHT = 1U << 13,  /** < Top Front Right */
    LEA_LOCATION_TOP_FRONT_CENTER = 1U << 14,  /** < Top Front Center */
    LEA_LOCATION_TOP_CENTER = 1U << 15,  /** < Top Center */
    LEA_LOCATION_TOP_BACK_LEFT = 1U << 16,  /** < Top Back Left */
    LEA_LOCATION_TOP_BACK_RIGHT = 1U << 17,  /** < Top Back Right */
    LEA_LOCATION_TOP_SIDE_LEFT = 1U << 18,  /** < Top Side Left */
    LEA_LOCATION_TOP_SIDE_RIGHT = 1U << 19,  /** < Top Side Right */
    LEA_LOCATION_TOP_BACK_CENTER = 1U << 20,  /** < Top Back Center */
    LEA_LOCATION_BOTTOM_FRONT_CENTER = 1U << 21,  /** < Bottom Front Center */
    LEA_LOCATION_BOTTOM_FRONT_LEFT = 1U << 22,  /** < Bottom Front Left */
    LEA_LOCATION_BOTTOM_FRONT_RIGHT = 1U << 23,  /** < Bottom Front Right */
    LEA_LOCATION_FRONT_LEFT_WIDE = 1U << 24,  /** < Front Left Wide */
    LEA_LOCATION_FRONT_RIGHT_WIDE = 1U << 25,  /** < Front Right Wide */
    LEA_LOCATION_LEFT_SURROUND = 1U << 26,  /** < Left Surround */
    LEA_LOCATION_RIGHT_SURROUND = 1U << 27,  /** < Right Surround */
    LEA_LOCATION_RESERVED = ((1U << 28) | (1U << 29) | (1U << 30) | (1U << 31)) /** < bit28 ~ bit29 */
};

Codec 模块关闭:

Codec模块的关闭接口,主要用户Codec模块的资源释放。

/**
 * @brief       Deinitialize LE Audio codec stream.
 * @return      none.
 */
void lea_codec_stream_deinit(void);

(3) Codec 设置输出音量

Codec模块的设置输出音量接口,主要用于设置音频输出的音量。

/**
 * @brief       Set output volume.
 * @param[in]   volume    - volume value to set.
 * @return      none.
 */
void lea_codec_set_output_volume(uint8_t volume)

Codec 不同实例定义:

Codec模块默认定义了几种不同的实例,分别对应不同的音频输入输出方式。目前SDK支持Codec(不同芯片或者模组下对应不同硬件Codec驱动)、USB Audio(USB音频设备,需要Demo中打开UAC功能)、Sine Wave(生成音频信号,用于测试音频播放功能,只支持输入)。

#define LE_AUDIO_CODEC_TYPE_NONE                0x00
#define LE_AUDIO_CODEC_TYPE_CODEC               0x01
#define LE_AUDIO_CODEC_TYPE_USB_AUDIO           0x02
#define LE_AUDIO_CODEC_SAMPLE_SINE_WAVE         0x03

#ifndef LE_AUDIO_CODEC_INPUT_TYPE
#define LE_AUDIO_CODEC_INPUT_TYPE LE_AUDIO_CODEC_TYPE_NONE
#endif

#ifndef LE_AUDIO_CODEC_OUTPUT_TYPE
#define LE_AUDIO_CODEC_OUTPUT_TYPE LE_AUDIO_CODEC_TYPE_NONE
#endif

用户可以根据需要,修改LE_AUDIO_CODEC_INPUT_TYPE的定义修改Input的类型,修改LE_AUDIO_CODEC_OUTPUT_TYPE的定义修改Output的类型。

Input 相关实例接口:

音频输入清理当前所有未处理的输入数据,并将未处理的数据丢弃。

/**
 * @brief       Clean input buffer.
 * @return      none.
 */
void lea_codec_input_clean_buffer(void);

音频输入使能和关闭

/**
 * @brief       Initialize input stream.
 * @return      none.
 */
void lea_codec_input_stream_init(void);

/**
 * @brief       Deinitialize input stream.
 * @return      none.
 */
void lea_codec_input_stream_deinit(void);

判断当前音频未处理的音频数据的Sample数,如果拥有足够的数据,分别写入Left和Right的音频数据缓冲区。

Sample数只有音频采样率有关,和音频采样深度、音频通道数无关。

/**
 * @brief       Get input audio data.
 * @param[out]  left_data    - pointer to left channel audio data buffer.
 * @param[out]  right_data   - pointer to right channel audio data buffer.
 * @param[in]   sample_num   - number of samples per channel to read.
 * @return      true if data is successfully read, false otherwise.
 */
bool lea_codec_input_get_audio_data(int16_t *left_data, int16_t *right_data, uint16_t sample_num);

Output 相关实例接口:

音频输出使能和关闭

/**
 * @brief       Initialize output stream.
 * @return      none.
 */
void lea_codec_output_stream_init(void);

/**
 * @brief       Deinitialize output stream.
 * @return      none.
 */
void lea_codec_output_stream_deinit(void);

音频输出设置音频数据,将特定数量的Sample数据写入音频输出缓冲区。Left Data是左声道音频数据,Right Data是右声道音频数据,Sample Num是Sample数。

/**
 * @brief       Set output audio data.
 * @param[in]   left_data    - pointer to left channel audio data.
 * @param[in]   right_data   - pointer to right channel audio data.
 * @param[in]   sample_num   - number of samples per channel.
 * @return      none.
 */
void lea_codec_output_set_audio_data(int16_t *left_data, int16_t *right_data, uint16_t sample_num);

(4) Input 和 Output 共同的实例接口

只有LE_AUDIO_CODEC_INPUT_TYPE和LE_AUDIO_CODEC_OUTPUT_TYPE选择相同的音频实例,并且音频任务同时启动输入和输出时,为了兼容有些Codec实例不支持输入和输出分开初始化,提供一个共同的初始化接口。

/**
 * @brief       Initialize both input and output stream.
 * @return      none.
 */
void lea_codec_in_output_stream_init(void);

/**
 * @brief       Deinitialize both input and output stream.
 * @return      none.
 */
void lea_codec_in_output_stream_deinit(void);

(5) Audio Common 模块

该模块主要对于音频的使用场景进行了抽象,分为音频输入和音频输出两个方向,在不同的角度或者维度来看,音频输入可能刚好是呈现镜像。

注意

  • 当前 SDK 的统一逻辑:从外部 Codec 采集原始音频,通过 LC3 编码器编码后,在通过 LE Audio 协议发送出去的行为称为音频输入;从 LE Audio 协议接收到编码数据,并通过 LC3 解码器解码后,在播放到外部 Codec 播放的行为称为音频输出。

四个典型的音频场景:

  • Broadcast Source:广播源,只有音频采集并播放的行为,所以只有音频输入的功能。
  • Broadcast Sink:广播接收器,只有接收 BIS 上的音频并播放的行为,所以只有音频输出的功能。
  • Unicast Client:单播客户端,给已连接的设备,播放音乐的场景,只有音频输入的功能;双向通话的场景,同时有音频输入和输出的功能;单向麦克风音频采集的场景,只有音频输出的功能。
  • Unicast Server:单播服务端,与 Unicast Client 的逻辑刚好相反。例如,播放音乐的场景,只有音频输出的功能。

Audio Common 模块的逻辑框图如下图:

Audio Common模块逻辑框图

不同的 Audio Task 通过一些 API 配置当前音频场景下的音频功能和参数,Audio Common 模块根据配置信息,初始化不同的 LC3 编解码器,并通过 ISO Data 模块获取和发送编码后的音频数据。

LE Audio 的音频参数的结构体定义如下:

blocks:LC3 编码器的块数,目前仅支持 1。

location:音频的位置信息,用于标识音频的位置信息,目前仅支持左耳(0x01),右耳(0x02),立体声(0x03)。

samplingFrequency:音频采样率。

frameDuration:LC3 编码器的帧时长,目前仅支持 10ms(0x01)和 7.5ms(0x02)。

frameOctets:LC3 编码器的帧数据长度,单位为字节。

iso_handle:ISO Data 模块的句柄,用于标识当前音频的 ISO Data 句柄。

presentationDelay:播放延迟,单位为微秒,目前仅在 output 上使用。

struct lea_config { // refer to struct lea_bmr_config,
    uint8_t  blocks;
    uint32_t location;
    uint8_t  samplingFrequency;
    uint8_t  frameDuration;
    uint16_t frameOctets;

    uint16_t iso_handle;
    uint32_t presentationDelay;
};

Input 相关接口:

初始化 Input 相关的配置信息,主要是 Audio Common 内部使用的配置信息。

/**
 * @brief       Initialize LE Audio input configuration cache.
 * @return      none.
 */
void lea_input_config_initial(void);

设置和释放 Input 相关的配置信息,必须要在 lea_set_input_config 之前配置完成,主要是分配 LC3 编解码器的数量和空间。

/**
 * @brief       Configure LC3 encoder workspace for all input locations.
 * @param[in]   location    - bitmap of LE Audio locations.
 * @return      0 on success, negative value otherwise.
 */
int lea_set_input_all_location(uint32_t location);

/**
 * @brief       Release LC3 encoder workspace allocated for input.
 * @return      0 on success, negative value otherwise.
 */
int lea_release_input_location(void);

/**
 * @brief       Program input sampling count and interval based on BAP config.
 * @param[in]   frequency   - LC3 sampling frequency selector.
 * @param[in]   duration    - LC3 frame duration selector.
 * @return      none.
 */
void lea_set_input_sample_config_bap(uint8_t frequency, uint8_t duration);

打开和关闭Input功能。

/**
 * @brief       Enable audio input path and reset acquisition timer.
 * @param[in]   get_time    - reference timestamp (optional).
 * @return      none.
 */
void lea_open_input(uint32_t get_time);

/**
 * @brief       Disable audio input path and clear ASE state.
 * @return      none.
 */
void lea_close_input(void);

初始化和释放特定ISO通道的配置。

/**
 * @brief       Store ASE specific input configuration and start LC3 encoder.
 * @param[in]   p_config    - pointer to ASE configuration.
 * @return      none.
 */
void lea_set_input_config(const struct lea_config *p_config);

/**
 * @brief       Remove stored input configuration for specific ISO handle.
 * @param[in]   iso_handle  - ISO connection handle.
 * @return      none.
 */
void lea_release_input_config(uint16_t iso_handle);

Output 相关接口:

Output的接口和Input完全相同,简单的镜像关系。

(6) Audio Task 模块

Audio Task目前支持四个LE audio的场景,每个场景对应的源代码如下:

  • Broadcast Source:(tlkmdi_le_bms.c) Broadcast Media Sender(BMS) 广播音乐发射器,只需要播放音乐,不需要接收音频。

  • Broadcast Sink:(tlkmdi_le_bmr.c) Broadcast Media Receiver(BMR) 广播音乐接收器,只需要接收音频,不需要播放音乐。

  • Unicast Client:(tlkmdi_le_uc.c) Unicast Client 单播客户端,基于LE Audio协议,支持连接TWS和Headset耳机,播放音乐或双向通话等场景。

  • Unicast Server:(tlkmdi_le_us.c) Unicast Server 单播服务端,基于LE Audio协议,支持连接手机或者标准UC设备,播放音乐和双向通话等场景。

关于Audio Task的解释,可以参考Handbook中的相关章节。

单 controller相关

概述

本文主要对Bluetooth Audio SDK中的Bluetooth controller的使用方式进行阐述,并对如何使用bluez进行验证进行说明。

配置Bluetooth controller

vendor/bluetooth_controller 目录下的 app_config.h 文件中,可以配置 Bluetooth controller 使用的串口引脚及波特率,目前只支持在TLSR952X平台下使用该项目。

#define TLKHW_TYPE      TLKHW_TLSR9528A_EVK_C1T266A20
#define HCI_TR_RX_PIN   GPIO_FC_PC7
#define HCI_TR_TX_PIN   GPIO_FC_PC6
#define HCI_TR_BAUDRATE (1000000)
#define LE_HOST_SEND_HCI_MODE LE_HOST_SEND_HCI_MODE_NONE

配置说明

  • TLKHW_TYPE:指定使用的硬件平台。
  • HCI_TR_RX_PIN:指定串口接收引脚。
  • HCI_TR_TX_PIN:指定串口发送引脚。
  • HCI_TR_BAUDRATE:指定串口波特率。
  • LE_HOST_SEND_HCI_MODE:指定发送 HCI 数据的方式,使用默认的即可。

bluez验证

(1) 连接controller

将controller通过串口与电脑连接:

ls /dev/ttyUSB*  # 查看电脑是否识别到usb串口

安装cutecom测试串口与controller是否通信正常:

sudo apt install cutecom
sudo cutecom  # 不加sudo会没有操作串口的权限

打开cutecom后,设置波特率,并连接上一步获取到的ttyUSB*,将接收和发送模式都切换为HEX,然后任意发送一条HCI命令,查看controller是否正确执行,例如:

  • HCI_RESET: 01 03 0c 00
  • HCI_READ_LOCAL_NAME: 01 14 0c 00

将串口连接至HCI并设为默认的蓝牙设备:

hciconfig  # 查看当前hci设备
sudo hciconfig hci0 down  # 关闭这个hci设备
sudo modprobe -r btusb  # 卸载usb蓝牙 重新启用usb蓝牙使用:sudo modprobe btusb

方式一(不阻塞命令行):

sudo hciattach /dev/ttyUSB0 any 1000000  # 以指定波特率来连接第一步获取到的usb串口至hci

注意

  • 该方式关闭和打开都需要另外输命令,关闭蓝牙之后会继续占用串口,若需切换波特率等操作需重新插拔串口。

方式二(支持快速关闭):

sudo btattach -N -B /dev/ttyUSB0 -S 1000000

注意

  • 关闭直接使用ctrl+c,退出后不会继续占用串口。
hciconfig  # 查看是否挂载成功,状态是否为up,若不是则进行debug

重要提示:连接成功后不要通过电脑的蓝牙设置界面来开关蓝牙,否则会打开电脑自带的蓝牙,应使用 hciconfig up/down 命令来开关蓝牙。

(2) 调试操作

安装wireshark:

sudo add-apt-repository ppa:wireshark-dev/stable  # 添加ppa源
sudo apt update
ls /etc/apt/sources.list.d  # 找到上一步添加的wireshark的源
sudo vim /etc/apt/sources.list.d/wireshark-dev-ubuntu-stable-noble.sources  
# 将其中的http://ppa.launchpad.net替换为http://launchpad.proxy.ustclug.org
sudo apt install wireshark
wireshark  # 启动wireshark

抓取蓝牙log:

sudo btmon -w <文件名>  # 收集蓝牙log信息并输出到文件中

开启蓝牙设备:

sudo hciconfig hci0 up

解析log与功能测试:

  • 通过wireshark打开抓取到的蓝牙log进行解析

  • 通过电脑的蓝牙设置界面打开蓝牙,进行搜索连接等操作

  • 测试播放音乐、传输文件等功能是否正常

TPSLL audio dongle

概述

本章节主要涉及TPSLL Audio dongle 参考设计,由于该部分功能不能单独使用,必须配合BT/TPSLL Headset参考设计或BT/TPSLL TWS参考设计一起才能正常工作,在BT/TPSLL Headset 参考设计BT/TPSLL TWS参考设计的相关章节中都有详细的介绍,这里不再赘述,请知悉;

Recording Card

本章节以TL751X为例介绍录音卡工程。

硬件介绍

泰凌TL751X录音卡开发板实物图如下图所示,购买方式及具体原理图请联系对接的FAE获取。

开发板型号:TL7519H-ML9118A C1T368A87_V1_2—2026-03-12

现对开发板上的各个模块(SDK已使用的部分)进行简单介绍,方便用户快速了解和上手。

TL751X录音卡开发板实物图

(1) USB接口

开发板接口功能如下图所示:

开发板接口功能

如上图所示,开发板从从上到下的USB功能分别为:

1) 红框处为Wi-Fi芯片通路,其经过usb转串口芯片与TLSR9118直接对接,用于Wi-Fi固件下载及日志输出。

2) 绿框为TL751x的USB1接口,用于TL751X的usb日志输出。

3) 黄框为TL751x的USB0接口,用于枚举成U盘功能,进行文件操作。

  • 灰框标注的按键为TL751x的复位按键。

  • 黄框标注的按键实际连接PB3引脚,定义为按键1(key1)。

  • 绿框标注的按键实际连接PB1引脚,定义为按键2(key2)。

LED灯编号与定义:

  • 蓝框标注的LED灯实际连接PC0引脚,定义为LED_BLUE。

  • 红框标注的LED灯实际连接PC1引脚,定义为LED_RED。

  • 按键和LED灯在SDK中的具体使用将在后续章节详述,本章仅对硬件部分进行初步介绍。

(2) SDIO接口

TL751x支持SDIO接口,硬件开发板提供两种类型的存储器件接口。

  • eMMC器件(默认):开发板板载了一块emmc接口的存储器件,接线方式如下图所示:断开绿框拨码开关,连接红框中所有的跳线帽,将灰框中的emmc_3v3和3v3供电相连,此时黄框中的存储芯片供电正常并可与TL751x通讯。

eMMC器件

  • SD卡:若想采用SD卡,接线方式见下图,打开绿框拨码开关,断开红框中所有的跳线帽,将灰框中的SD_3v3和3v3供电相连,此时黄框中的SD卡(需额外插卡)供电正常并可与TL751x通讯。

SD卡器件

软件配置将在后续章节叙述。

(3) Wi-Fi模块通信接口与配置

如需Wi-Fi功能,需按照下图方式对拨码开关进行拨片操作,芯片之间主要通过UART和SPI进行通讯。

通信拨码开关实物图

芯片引脚映射图

(4) 天线硬件

开发板设计上支持两种天线方案:

  • 独立天线(默认):蓝牙芯片和Wi-Fi芯片各自使用自己的独立芯片,断开R110、R111两个 0 \(\Omega\) 电阻

  • 共享天线:蓝牙芯片和Wi-Fi芯片共用一根天线,分时复用,在R110、R111两个位置焊上0电阻(使其导通)即可。

R110、R111位置如下图红框所示。

在独立天线模式下在黄框处(两处)分别接入胶棒天线,左侧对应Wi-Fi天线,右侧对应蓝牙芯片天线。

在共享天线模式下只需在绿框处接入胶棒天线即可。

天线接口分布示意图

(5) MIC 扩展板

如需使用多MIC功能,硬件上需外接MIC扩展子板一块(C1TXA87_V1_0)

MIC扩展子板实物图

固件编译与烧录

(1) 不包含 Boot 的编译与烧录

TL7519H编译:

录音卡工程默认使用bootloader启动,如果只是在工程开发和验证阶段,暂时不能用bootloader启动(可能会导致RTC等依赖boot的功能异常),需要进行如下操作:

1) 在app_config中将TLK_MW_USER_CTRL_ENABLE宏置为0

2) 编译:recording_card工程

  • merge_bin.sh脚本将会合并打包D25F和N22的固件:recording_card_n22_controller_120.bin

  • 直接在0地址烧入recording_card_n22_controller_120.bin文件即可。

  • 选择芯片型号TL751x,选择需要下载的文件,点击SWS,点击Activate,点击Download。完成下载。

不包含 Bootloader 的编译与烧录

或者在0地址下载recording_card.bin,在0x100000地址下载controller.bin即可。Merge脚本的本质是将两个固件合并,并在中间填充0XFF,将两次烧录过程变为一次烧录。

TL7519 DSP固件烧录(重要)(如果需要DSP功能)

使用BDT工具,选择Tool,下拉选项中选择TWS Tool,地址填入0x00200000,点击Browser选中对应的DSP bin文件,勾选后点击Download进行下载。

固件位置为在SDK的dsp/bin/目录下,请根据需要选择对应的固件,见后续Audio通路章节。

DSP固件下载说明

注意

  • DSP支持打包模式下载,如采用带boot方式启动,则shell脚本会自动打包DSP固件。但如采用不带boot的模式,仍需要手动在0x00200000下载。

(2) 含Boot编译与烧录

如果使用默认的 bootloader 启动方式,则需要进行如下操作:

1) 确认下面两个宏定义是默认打开的:

#define TLK_MW_USER_CTRL_ENABLE             (1)
#define TLK_MW_OTA_ENABLE                   (1 && TLK_MW_USER_CTRL_ENABLE)

2) 编译完工程,产生 d25f 的固件。

3) 在编译完固件后,需要执行 shell/ota/main_rc_751x_ota.sh 脚本(使用时注意脚本中文件的路径是否正确),会在同级目录下生成 recording_card_ota_firmware.bin 文件,该固件可直接用于 OTA。

4) 在 0 地址烧录 bootloader 固件。

5) 从 0x12000 地址下载 recording_card_ota_firmware.bin

第 3)~5) 步也可替换为运行 main_rc_751x.sh,该脚本会将 boot 和 app 固件打包整合一体,直接将打包后的固件从 0 地址下载即可。

如果单独给 Wi-Fi 升级,通过执行 main_recard_ota_wifi 脚本转换生成需要升级的 Wi-Fi OTA bin 文件即可:(注意文件路径)。

SDK 功能

(1) BLE 功能

用户可通过TelinkRecordCard APP进行功能体验。

设置界面:

首先需要打开Setting界面,确认选择了auto request mtu,并且MTU数值大于203;Opus Decode Setting配置为16kHz采样率,channel count 1

Opus 解码与 MTU 参数设置

Opus 解码与 MTU 参数设置

App Actions:

  • Opus Decoder提供demo:APP控制设备进行录音,设备通过BLE将录音信息传输给App解码,App存储录音文件。

  • File Transfer提供demo:用户通过BLE或者Wi-Fi读取设备的文件列表并进行文件下载。

  • Timer sync提供demo: App通过BLE给设备授时。

设置连接:

设备上电默认会发送BLE广播,广播默认名称:Xyris.

App切换到ADV界面,配置过滤(可基于名称或mac地址),点击刷新,选择对应的设备,即可完成连接。

ADV界面连接设置

实时录音 (Opus Decoder) 界面:

如下图所示,用户可以在 App 端点击 Start 开始录音,Stop 停止录音,在录音期间 App 会实时播放录音数据。在停止录音后,可以点击 Save 将之前的录音内容进行存储。

Opus 解码界面

文件下载 (File Transfer) 界面:

如下图所示,用户可以在 App 端获取设备端文件列表,对文件进行重命名、删除、下载操作。

文件下载与列表管理界面

(2) 按键功能

按键行为定义如下,配置代码见 app_rc_ui_key.capp_rc_ui_key_plan0.c

按键模式 单击 双击 三击 四击 长按后松开
Key1 开始/停止录音 开启/关闭 BLE 使能 DSP 功能 开启 WAV 写文件操作
Key2 打开 Wi-Fi 关闭 Wi-Fi 失能 DSP 功能 关闭 WAV 写文件操作 开启/关闭 U 盘模式

(3) LED 功能

红灯用于指示录音状态,蓝灯用以指示 BLE 状态。配置代码见 app_rc_ui_led.c

LED 关闭 长亮 呼吸 慢闪
红灯 关机 就绪状态(尚未录音) 录音中 /
蓝灯 BLE 关闭 BLE 已连接 / BLE 广播(未连接)

(4) 文件系统功能

SDK 基于 Fatfs 提供了一套文件读写接口,并将 diskio 抽象方便客户二次开发。具体内容见 tlkmw/file 文件夹下,用户可在 tlkmw_fs_diskio.hdiskio 进行配置。tlkmw/file/drc 中提供了多种 disk 接口可参考,以 eMMC 为例,io 配置如下。

static sdmmc_pin_config_t sdmmc_emmc_pin_config = {
    .sdmmc_clk_pin  = GPIO_FC_PG0,
    .sdmmc_cmd_pin  = GPIO_FC_PB7,
    .sdmmc_rst_pin  = GPIO_FC_PG5,
    .sdmmc_ds_pin   = GPIO_NONE_PIN,
    .sdmmc_dat0_pin = GPIO_FC_PG3,
    .sdmmc_dat1_pin = GPIO_FC_PG2,
    .sdmmc_dat2_pin = GPIO_FC_PG1,
    .sdmmc_dat3_pin = GPIO_FC_PG4,
    .sdmmc_dat4_pin = GPIO_NONE_PIN,
    .sdmmc_dat5_pin = GPIO_NONE_PIN,
    .sdmmc_dat6_pin = GPIO_NONE_PIN,
    .sdmmc_dat7_pin = GPIO_NONE_PIN,
};

SDK 基于 eMMC 接口驱动,封装函数,并定义 tlkmw_fs_diskio_t 的全局变量供底层文件系统调用即可完成文件系统的挂载。

const tlkmw_fs_diskio_t gTlkmwFsDiskIoEmmc = {
    .init = tlkmw_fs_drv_emmc_init,
    .sleep = tlkmw_fs_drv_emmc_sleep,
    .awake = tlkmw_fs_drv_emmc_awake,
    .write = tlkmw_fs_drv_emmc_write,
    .read = tlkmw_fs_drv_emmc_read,
    .getSectorSize = tlkmw_fs_drv_emmc_get_sector_size,
    .getSectorNum = tlkmw_fs_drv_emmc_get_sector_num,
};

如想使用 SD 卡,可在 app_config 中开启 TLK_CFG_FS_SDCARD_DISKIO_ENABLE,重载 tlkmw_fs_diskio_t 的全局变量为 SD 卡的对应接口。

(5) U 盘功能

设备支持 U 盘的功能,通过 USB1 口,连接上电脑会枚举成 U 盘操作,用户可以直接在电脑端对文件进行操作。U 盘默认为只读模式(可基于 TLK_USB_MSC_READ_ONLY 宏配置)。(如关闭只读模式,请不要在电脑端进行格式化操作,原因:未选择与 MCU 本地的文件系统匹配的 FAT 分区格式,会导致文件系统崩溃)。

当前 U 盘模式已改为手动开关模式,默认关闭。用户可通过 UI 开启/关闭 U 盘模式。U 盘和本地文件操作进行了互斥保护,当正处于录音、文件传输模式时,U 盘模式无法开启;当 U 盘模式开启后,录音和文件操作请求将会被拒绝。

U 盘文件列表展示

(6) OTA功能

OTA可通过TelinkBootOTA app进行体验。

使用流程:将要升级的recard_ota_firmware.bin 放到手机的文件夹下,然后连接APP选择要升级的固件点击开始即可。

(7) Audio通路及多麦BBF功能

Audio通路简介:

外部的声音被MIC或MIC阵列采集为16kHz采样率、16bits位深的数字信号;经过盲源波束形成(BBF)对声源方位的语音信号进行增强(单MIC不支持BBF);然后通过神经网络降噪(NN_NS)降低环境噪声信号;在经过自动增益控制(AGC)自动调节音量后,最终经由OPUS编码进行本地保存或通过RF传输出去。同时可通过SPI接口对整个音频节点链路数据提取出来进行分析。

TL751x 录音卡音频链路示意图

关于该示意图简单介绍如下:

  • BBF (blind beamforming) 盲源波束形成,是指在信号的理论模型和源信号无法精确获知的情况下,通过 MIC 阵列获取声场信息,对信源语音目标进行叠加增强,对噪声进行抑制。主要作用是融合多个通道的数据,对噪声和干扰方向进行抑制,叠加增强目标方向的信号。

TL751X录音卡方案支持2MIC/4MIC /6MIC 的BBF算法,在DSP端实现。下图为盲源波束形成示意图

波束形成与噪声抑制原理示意图

  • NN_NS (NN-based Noise Suppression) 为神经网络降噪,主要作用是通过数据监督模式下,学习语音与噪声的非线性关系,能够更精确地估计语音特征并抑制噪声,同时保持语音的自然感和清晰度。TL751X 录音卡方案 NN_NS 算法,在 DSP 端实现。

  • VAD (Voice Activity Detection) 为语音端点检测技术,主要作用是从带有噪声的语音中准确的定位出语音的开始和结束点,也就是把静音和实际语音分离出来,在静音阶段可以节约宝贵的算力、存储和带宽资源。

  • AGC (Automatic Gain Control) 为自动增益控制,主要作用语音达到一定的音量水平,不会因发言者与麦克风的距离改变时,声音有忽大忽小声的缺点,该算法在 D25F 端实现。下图是 AGC 效果示意图。

AGC 效果示意图

单麦/多麦方案:

EVB 硬件版本默认单麦录音方案,如果使用多麦的功能时,需确认一些硬件设置。

单 MIC 方案硬件设置:

单麦方案中,使用的是 EVK 板上默认的 MIC,使用前需确认一下配置:

名称 管脚 0 管脚 1 状态
电源 J13_1 J13_2 短接
DMIC1_CLK1 J37_9 J37_10 短接
DMIC1_DATA1 J37_11 J37_12 短接

如下图所示

多 MIC 方案主板实物图

多 MIC 方案主板引脚映射

多 MIC 方案硬件设置:

在多 MIC 方案中,需使用外接 MIC 子板。同时录音卡主板硬件也需做相应的设置。录音主板 EVK 设置如下表:

名称 管脚 0 管脚 1 状态
电源 J13_1 J13_2 断开
DMIC0_CLK0 J37_5 J37_6 断开
DMIC0_DATA0 J37_7 J37_8 断开
DMIC1_CLK1 J37_9 J37_10 断开
DMIC1_DATA1 J37_11 J37_12 断开
TL_PB4 J38_3 J38_4 断开
TL_PB6 J38_5 J38_6 断开

主板和 MIC 扩展板接线见下表

2MIC BBF 录音主板与多麦克风子板接线表:

2MIC

4MIC BBF 录音主板与多麦克风子板接线表:

4MIC

6MIC BBF 录音主板与多麦克风子板接线表:

6MIC

6MIC 方案主板与子板连接实物图

6MIC 方案引脚图

6MIC 方案引脚图

软件配置:

通过下面的宏定义及配置可以实现单麦或多 MIC 录音,下面展示的是配置 6 麦录音的代码。使用时需注意:硬件设置和软件配置必须严格一致

///disable and CHN for BBF

#define TLKALG_BBF_DIS            0
#define TLKALG_BBF_2CH_EN         2
#define TLKALG_BBF_4CH_EN         4
#define TLKALG_BBF_6CH_EN         6

///BBF

#define TLKALG_BBF_ENABLE         TLKALG_BBF_6CH_EN     ///config dis or CHN

功能使用:

上电运行后(建议进行彻底的断电重启以确保初始化正常),基于 UI 表,三连击 KEY0 就可以测试单麦(带 NN-NS 算法)或多麦录音功能(带 BBF 和 NN-NS 算法),三连击 KEY1 就可以关闭相应的 BBF 及 NN-NS 算法功能。

(8) Wi-Fi 快传功能

Wi-Fi 固件及 SDK 请联系对接的 FAE 获取。

SDK 支持共享天线和独立天线两种模式:

  • 独立天线下,BLE 连接和 Wi-Fi 连接同时存在,手机 APP 使用 BLE 通路通知设备需要下载的文件,再通过 Wi-Fi 通路接收文件数据。Wi-Fi 连接后,APP 端 UI 界面点击下载改走 Wi-Fi 通路。
  • 共享天线下,BLE 连接和 Wi-Fi 连接只能保持其中一路连接,原有的 BLE 命令通路通过 Wi-Fi 通路透传转发,APP 端可在点击切换按钮切换至 Wi-Fi 通路进行快传功能。

(9) BT 耳机通路功能

本功能专为部分客户定制开发,如果您无该功能需求,请忽略本章节内容。

本 SDK 提供了连接 BT 耳机通路的示例程序,暂时未支持经典蓝牙和低功耗蓝牙时序共存,请确保在使用过程中仅存在一种空中链路(即:传统录音模式和 BT 模式不可共存),否则可能会引起异常。BT 链路使用方法如下:

  • app_config.h 中配置 TLK_RC_CFG_BT_CENTRAL_STREAM 为 1,打开 BT 中心设备的相关功能,进行固件编译和 burn。

  • 通过 USB shell 完成与 BT 耳机的连接(代码逻辑见 tlkusb_debug_shell_hook):

    a. 输入 11 01 00 00 关闭 BT SCAN。(减少带宽分配,以更快搜到设备)。

    b. 确保 BT 耳机进入配对模式,设备端输入 11 02 04,开启 BT INQUIRY 搜索耳机。

    c. Log 上发现设备已搜索到想连的耳机后,输入 11 03 关闭 BT INQUIRY。

    d. 输入 11 04 可以打印已搜索到的设备。

    e. 输入 11 05 XX 连接搜索到的、且想要连接的设备,XX 为序号(需基于第 d 步打印出的设备列表)。

耳机搜索日志

设备列表打印日志

如图所示,输入 11 05 02 连接 AirPods Pro。(AirPods Pro 序号为 2)

若耳机设备已和开发板配对过,则无需 a~e 步,可直接开启 SCAN(上电自动开启并持续 120s 或手动开启),将对端耳机开盖,等待耳机端发起自动回连即可。

  • 通过 USB shell 可触发 A2DP/SCO 示例程序(代码逻辑见 tlkusb_debug_shell_hook):

输入 11 07 可开启 A2DP 音乐示例程序,输入 11 08 则关闭该场景。A2DP 模式下,耳机端将收到录音卡发送的正弦波音频。用户可基于自己的音频需求,灵活修改 tlkmdi_a2dp_out_read_samples 函数,修改获取音频流的实际处理方式以替代正弦波。

输入 11 09 可开启 SCO 话音示例程序,输入 11 0a 则关闭该场景。SCO 话音模式下,设备会将从耳机端收到的音频数据(由耳机麦克风采集)与正弦波混音后,回环传输给耳机播放。用户可根据实际产品需求,修改 tlkmdi_record_fill_spk_data_to_uac 函数内部的混音逻辑和调用位置,进行二次开发。

(10) BT 手机通路功能

本功能专为部分客户定制开发,如果您无该功能需求,请忽略本章节内容。

本 SDK 提供了连接 BT 手机通路的示例程序,暂时未支持经典蓝牙和低功耗蓝牙在高带宽场景下时序共存,可能会引起异常。BT 手机链路使用方法如下:

  • app_config.h 中配置 TLK_RC_CFG_BT_PERIPHERAL_STREAM 为 1,打开 BT 外围设备的相关功能,进行固件编译和 burn。

  • 芯片上电后的 120s 内会自动开启 page scan/inquiry scan,手机可以在蓝牙界面连接本设备,并建立 ACL 及 HFP profile 连接。

  • 当手机端有电话存在时,会与本设备建立 SCO 连接,设备端 Speaker 播放对端声音,并传输 MIC 采集的数据给对端。Speaker 所播放的 PCM 可基于 bt_audio_get_spk_data_cb 该函数获取,MIC 数据基于 bt_audio_get_mic_data_cb 该函数获取。

注意

  • 由于录音卡的开发板未引出 Speaker,本示例程序目前在 TL751X 的标准 EVK:C1T368A20 上进行了适配,开启 TLK_RC_CFG_BT_PERIPHERAL_STREAM 将自动选择该开发板。(请忽略前序章节所有的接线方式)

代码架构

(1) 目录简介

如下图所示,工程目录下包括 app_recording_card 目录、ble 目录及其余多个文件。

app_recording_card 目录下包含录音卡的逻辑代码,ble 目录下包含项目所有 BLE 逻辑代码,两者在架构上拆分为两个独立线程运行。

app_config.h 内部对项目的宏进行配置,main.c 文件内为主函数和 MCU 平台配置。

工程目录

app_recording_card 目录:

app_recording_card 目录结构如下图所示:

app_recording_card 目录

如图所示,app_recording_card 目录下包括 data_pathlogicthread_coreui 和 API 等路径/文件。

  • readme.md 文件中阐述了部分名词解释和 UI 逻辑。

  • app_recording_card_api 提供相关接口供按键 UI/BLE CMD 调用。

  • data_path 文件夹内负责抽象各类数据通路,包括录音输入流(stream in)、BLE 上传流(stream out)、Wi-Fi 上传流(stream out)、文件读取流。in 代表该输入流可写入,out 代表该输出流可读取。

  • logic 文件夹内部主要为业务逻辑,包括录音开始/结束、数据流传输等逻辑。

  • thread_core 文件夹内为该线程的核心代码,包括线程的定义和异步消息处理逻辑。

  • ui 文件夹下包括按键和 OLED(暂未生效)等 UI 逻辑代码。

BLE 目录:

BLE 目录结构如下图所示:

BLE 目录

  • app_ble.c 负责设置 BLE 的广播内容,包括名称,SN 号等信息。
  • app_ble_telink_server.c 主要是与 Telink Opus Decode APP 连接的命令和响应的处理。
  • app_ble_telink_command.c 是对于 Telink APP 的命令的处理函数,例如查询 Opus 文件列表,读取文件信息,删除文件等。
  • app_ble_server.c 主要是与 Xyris APP 连接的命令和响应的处理。
  • app_ble_command.c 是对 Xyris APP 的命令的处理函数,目前实现 APP 支持的一些命令。

BT 目录:

app_bt.c 负责设置 BT 相关的回调函数注册以及启动钩子配置。

(2) 设计思路

该工程设计上采用多线程并发思想,不同线程之间使用消息/事件机制异步触发,减少耦合度并确保线程安全,整体软件架构框图如下图所示。

整体架构框图

  • audio 线程负责录音,音频降噪和数据编码,其异步接收录音卡线程发送来的开启/停止命令,当系统不处于录音状态时该线程阻塞。当处于录音状态时,其定时将录音数据编码并推入共享 FIFO,并唤醒录音线程进行数据分发(存入文件系统/通过 BLE 上传给手机 APP)。

  • 系统(sys)线程主要负责日志、按键、SQL 存储及 USB 相关代码模块的运行。

  • ble controller 线程/核心负责 BLE 底层的链路管理和收发包逻辑,在软件层面其只与 ble host 线程进行交互。

  • ble host 线程负责与手机 APP 进行应用数据的交互,其负责解码手机 APP 的指令并重新封装成 CMD 并通过消息队列发送给录音卡线程处理。当录音卡将命令异步处理完成后,会发送 CMD ACK 回复给 ble host 线程,其再次封装后传递给手机 APP。该线程也接收录音卡线程分发过来的实时录音数据并上报给手机 APP。

  • 录音卡线程主要工作如下:

    • 接收ble host线程发送来的指令,如开启录音/上报 Opus 文件表/删文件等,异步处理后发送 CMD ACK。
    • 接收sys线程的按键 UI 逻辑,执行录音的启停。
    • 唤醒audio线程启动实时录音,读取 audio stream FIFO,进行数据分发(存入文件系统,BLE 实时上传)等。
    • 已录音文件的上传,可选 BLE 或 Wi-Fi 通路(SPI 转发)。

tlkapp.c 中为线程创建逻辑:

tlkapp.c 线程创建

自定义重载 user 线程

(3) 消息传递机制简介

为确保线程安全和避免各类临界区 bug,同时为了减少代码耦合,ble host 接收到 APP 指令并不直接执行录音卡的业务代码。而是封装成消息通过消息队列(邮箱)传递给录音卡线程进行异步处理,当异步处理完成,录音卡会回复对应的 CMD ACK。为代码解耦、低功耗集成,便于二次开发 code merge 至 SDK,强烈建议用户参考该模式进行二次开发。

线程外调用的 API 函数,均为发送邮件。

API 函数发送邮件

线程收到邮件,执行对应的业务逻辑。

收邮件后处理逻辑

BT Interphone

概述

本章节主要对 Bluetooth Audio SDK 中 BT Interphone 参考设计进行介绍。BT Interphone 是针对蓝牙对讲机/蓝牙头盔耳机等产品的音频应用,基于 Telink SDK 实现,提供经典蓝牙音频、Mesh 对讲、双连接管理等功能。支持同时连接手机(A2DP/HFP)和耳机设备,实现音乐播放、通话以及设备间对讲功能。

主要特性如下:

  • 经典蓝牙相关特性

    • 支持BT链路配对,回连,角色切换等功能
    • 支持安全加密(SSP),自适应跳频(AFH)
    • 支持经典蓝牙双连接(同时连接两路手机或一路手机一路耳机)
    • 支持A2DP SNK音乐播放(SBC/AAC解码)
    • 支持HFP HF/AG通话功能
    • 支持AVRCP媒体控制
    • 支持SPP数据传输
    • 支持远程设备名称过滤连接
  • LE 相关特性

    • 支持 BLE ACL Peripheral 连接
  • Interphone对讲特性

    • 支持Mesh网络对讲功能(依赖721x)
    • 支持I2S音频数据通路
    • 支持DSP音频处理(NN降噪等)
    • 支持音乐、通话、Mesh三种音频模式切换
    • 支持Sidetone侧音功能

目录结构

bt_interphone
├── app_acl_peripheral.c    // BLE ACL Peripheral初始化及连接管理
├── app_audio.c             // 音频任务创建及PCM数据回调注册
├── app_ble.c               // BLE协议栈初始化及HID/Headset功能注册
├── app_ble_headset.c       // BLE Unicast Server Headset初始化
├── app_ble_hid.c           // BLE HID音量控制功能
├── app_bt.c                // BT ACL、Profile连接/断连及重连处理
├── app_config.h            // 工程配置文件
├── app_config_ex.h         // 扩展配置文件
├── app_emi_bqb.c           // EMI/BQB测试相关
├── app_key_led_config.c    // 按键配置文件
├── app_product_test.c      // 产测功能
├── app_usb_shell.c         // USB调试Shell命令处理
└── main.c                  // 项目入口

BT Host

BT Interphone 的 BT Host 部分参考 btble headset 章节的 BT Host 接口全集,涵盖 ACL 及 Profile 连接/断连等关键事件。在此基础上,BT Interphone 增加了以下特性:

  • 注册 ACL 连接/加密/断开回调:
static void app_btmgr_aclConnectCB(uint16_t handle, uint8_t status, uint8_t *pBtAddr, uint8_t dtype, uint8_t hfp_ChId)

该函数是 ACL 连接完成事件在应用层的回调处理函数。BT Interphone 支持同时连接两种不同类型的设备(如手机和耳机),根据连接设备的类型动态调整扫描策略。

static void app_btmgr_aclEncryptCB(uint16_t handle, uint8_t status, uint8_t *pBtAddr, uint8_t dtype, uint8_t hfp_ChId)

该函数是链路加密完成事件的回调处理函数。在加密完成后发起 SDP 服务查询,并根据设备类型追加相应的 Profile。

static void app_btmgr_aclDisconnCB(uint16_t handle, uint8_t reason, uint8_t *pBtAddr, uint8_t dtype)

该函数是 ACL 断开完成事件的回调处理函数。断开时回收音频调度器资源,并在 ACL 超时掉线时触发回连。

  • 根据设备类型动态追加 Profile:
static void app_btmgr_appendProfile(uint16_t aclHandle)

该函数根据连接设备的类型(手机/电脑/耳机)动态追加 A2DP、HFP、AVRCP、HID 等 Profile。对于手机和电脑设备,会追加 A2DP SNK 和 HID Server;对于已配对设备,会根据保存的 RFC Channel 信息追加 HFP 等 Profile。

  • 远程设备名称过滤:
#if (TLK_CHECK_REMOTE_DEV)
void tlkmdi_btacl_getRemoteNameChange(uint8_t *pData)

该功能用于根据远程设备的蓝牙名称进行过滤,修改指定名称的设备的类型(如 "X5 9TWMCC"),可用于运动相机识别。

  • 开机回连与扫描控制:
void tlkapp_host_bt_taskStartHook(void)

该函数在 BT Host 任务启动时调用。如果上次配对的是手机等设备,则发起回连;如果是耳机等设备,则进入 Page Scan 模式等待连接。

LE Host

BT Interphone 的 LE Host 部分包含以下功能模块:

  • BLE ACL Peripheral
void app_ble_acl_peripheral_init(void);
void app_ble_acl_peripheral_start(void);

初始化 BLE ACL Peripheral 功能,设置广播参数并启动广播,支持 BLE 设备的连接和配对。

BLE 协议栈初始化在 app_ble.c 中完成:

void tlkapp_host_le_init(void)

该函数初始化 BLE 协议栈,设置 MAC 地址,注册 ACL Peripheral、HID 等服务。

Interphone 对讲模块

Interphone 对讲模块位于 tlkmw/audio/interphone/ 目录下,是 BT Interphone 的核心功能模块。

(1) 模块初始化

int tlkmdi_interphone_init(void);

该函数初始化 Interphone 模块,包括:

  • 初始化 Link Manager(蓝牙音乐链路管理)
  • 初始化 Mesh Manager(Mesh对讲管理)
  • 初始化 HF Manager(通话管理)
  • 初始化音频缓冲区
  • 初始化 I2S 接口
  • 初始化音频算法

(2) 音频模式控制

Interphone 支持三种音频模式:

  • MESH 模式:Mesh网络对讲
void tlkmdi_interphone_mesh_control(uint8_t isStart);

启动/停止 Mesh 对讲功能,控制 I2S 收发 DMA 的使能。

  • MUSIC 模式:蓝牙音乐播放
void tlkmdi_interphone_bt_music_control(uint16_t handle, uint8_t isStart);

启动/停止蓝牙音乐播放,支持 SBC 和 AAC 解码格式。

  • AG 模式:通话功能
void tlkmdi_interphone_voice_control(uint16_t acl_handle, uint8_t is_start, uint8_t codec);

启动/停止通话功能,支持 CVSD 和 mSBC 编解码。

(3) 操作接口

bool tlkmdi_interphone_operate(uint16_t handle, uint8_t opcode, uint8_t *pdata, uint16_t dataLen);

该函数提供 Interphone 的操作接口,支持以下操作码:

  • TLKAUD_OPCODE_VOLUME_INC:音量增加
  • TLKAUD_OPCODE_VOLUME_DEC:音量减少
  • TLKAUD_OPCODE_TRIGGER_CUSTOMIZED_PLAYPAUSE:播放/暂停切换

按键功能

按键号 单击 双击 三击 长按
KEY1 播放/暂停音乐 下一曲 开启配对
KEY2 接听最新的来电 上一曲
KEY3 音量+ 音量- 触发Siri
KEY4 挂断通话

配置文件

(1) app_config.h 关键配置

#define TLK_BT_MULTIPNT_ENABLE     (1)     // 开启双连接
#define TLK_STK_BTACL_NUMB 2               // ACL连接数量
#define TLK_STK_BTSCO_NUMB 2               // SCO连接数量

#define TLK_INTERPHONE_ENABLE    1         // 开启Interphone功能
#define TLK_CHECK_REMOTE_DEV     1         // 开启远程设备名称过滤
#define TLK_APP_REMOTE_NAME_DATA "X5 9TWMCC" // 指定过滤的设备名称

// BT Profile配置
#define TLK_STK_BT_ENABLE         1
#define TLKBTP_CFG_A2DP_ENABLE    1
#define TLKBTP_CFG_A2DPSNK_ENABLE 1
#define TLKBTP_CFG_HFP_ENABLE     1
#define TLKBTP_CFG_HFPHF_ENABLE   1
#define TLKBTP_CFG_HFPAG_ENABLE   1
#define TLKBTP_CFG_AVRCP_ENABLE   1
#define TLKBTP_CFG_SPP_ENABLE     1

// LE Audio配置
#define TLK_STK_BLE_ENABLE        1
#define TLK_MW_LEA_US_MUSIC_ENABLE 1
#define TLK_MW_LEA_US_VOICE_ENABLE 1

// Interphone I2S配置
#define TLKMW_INTERPHONE_EN       1
#define TLKMW_FIFO_IRQ_EN         1
#define TX_FIFO_IRQ_ENABLE        1
#define INTERPHONE_I2S_FIFO       FIFO3
#define TLK_PCM_DATA_WR_EN        1

(2) app_config_ex.h 扩展配置

#define TLK_CFG_BT_EX_PAIRING_MODE_ENABLE 1  // 开启扩展配对模式

音频任务创建

app_audio.c 中创建 Interphone 音频任务:

void app_audio_create_interphone_task(void)
{
    tlkapp_audioScheduler_taskInfo_t info = {
        .audioType = TLKAPP_AUDIO_SCHEDULER_AUDIO_TYPE_MUSIC,
        .optype    = TLKAUD_TYPE_INTRTPHONE,
        .priority  = tlkapp_audioScheduler_getDefaultPriority(TLKAUD_TYPE_INTRTPHONE),
        .state     = TLKAPP_AUDIO_SCHEDULER_TASK_STATE_IDLE,
    };
    uint32_t taskID = 0XFFFF + ((uint32_t)TLKAUD_TYPE_INTRTPHONE << 16);
    tlkapp_audioScheduler_updateTask(taskID, info, 0);
}

音频通路测试

(1) 蓝牙音频测试

单路蓝牙测试:

  • 手机音乐与通话测试

连接一路手机后,测试播放音乐以及拨打电话,确认上下行音频均正常。

  • 耳机通话测试

连接一路蓝牙耳机(或另一块开发板)后,通过 USB 发送命令 11 01 09 开启通话,确认双方均可听到声音。

双路蓝牙测试:

  • 音乐抢占测试

连接两路手机后,手机 A 先播放音乐,手机 B 后播放音乐,手机 B 会抢占手机 A 的音乐;暂停手机 B 后,音频会自动恢复到手机 A 继续播放。

  • 通话不抢占测试

手机 A 先打电话,手机 B 后打电话,手机 B 不会抢占手机 A 的通话;挂断手机 A 后会自动切回手机 B 的通话。

  • 通话抢占音乐测试

手机 A 先播放音乐,手机 B 后打电话,电话会抢占音乐,优先处理通话音频。

(2) Mesh 音频验证

通过短接 I2S ADC 和 DAC 引脚(PI3 和 PI4),可以听见自己说话的声音,音频通路为:MIC -> NN -> I2S OUT = I2S IN -> SPK。

通过 USB 指令控制 Mesh 组网的进入和退出:

功能 USB 指令
进入组网 11 10 24
退出组网 11 10 25

(3) Sidetone 验证

通过 USB 指令开启或关闭 Sidetone 功能,开启后可以听见自己说话的声音:

功能 USB 指令
开启 Sidetone 11 10 20
退出 Sidetone 11 10 21

(4) 提示音验证

按照提示音下载章节中指定的地址将提示音文件烧录至 Flash 后,在蓝牙连接、断开等场景下即可听到相应的提示音。