跳转至

B91 BT/低延时双模在线通⽤SDK


文档概述

本文对TWS耳机方案的实现方式进行阐述,对客户二次开发所关注的模块进行详细说明,以便客户对SDK能快速理解,加快项目立项到量产的时间周期。

BT/低延时双模在线通用SDK典型应用包括:传统单Bluetooth(BT)模式的TWS、2.4GHz低延时和BT双模同时在线的TWS、及配合TWS工作的2.4GHz dongle。具体可以实现以下应用:单独通过BT连接到PC或者手机进行电话和音乐;单独通过2.4GHz低延时私有协议连接dongle进行通话或者低延时音乐播放;通过BT和2.4GHz dongle两路混音播放。

主要支持的特性:

  • 支持2.4GHz低延时和BT双模在线的TWS模式
  • 支持配合双模在线TWS模式的dongle端功能
  • 支持单BT的TWS模式
  • 支持2.4GHz低延时私有协议
  • 支持BT HFP-HF, A2DP-SINK, AVRCP, SPP, GATT协议
  • 支持基于BT SPP/GATT的无线升级功能
  • BT音乐支持AAC SBC格式解码
  • BT通话支持MSBC CVSD编码解码
  • 2.4GHz低延时语音支持LC3编解码
  • 支持音频EQ,音乐模式支持9阶, 通话模式上行支持4阶,通话下行支持4阶
  • 支持BT通话丢包补偿机制(PLC) MSBC及CVSD编码格式
  • 支持ADPCM8格式的tone音播放
  • 支持MIC自动增益控制(AGC)
  • 支持动态调整音频信号范围(DRC)
  • 支持自动回声消除功能(AEC)
  • 支持噪声抑制功能(NS)
  • 支持BF波束成形功能(BF Beamforming)

快速上手

代码结构说明

Src下根目录结构如下:

根目录结构

  • application:音频通路及USB audio相关。
  • boot:启动文件
  • codec:codec相关
  • common:编译调试相关代码
  • drivers:驱动相关
  • proj_lib:算法库
  • stack:BT及async协议栈相关代码
  • tlkapi:api层代码
  • vendor:存放build工程代码,目前包含_proj_boot_device_、 _proj_bt_tws_ 、_proj_cc_tws_、_proj_le_ll_dongle_四个工程编译选项。

软件编译环境

软件编译环境采用Telink_RDS IDE,可以从如下地址下载安装: http://wiki.telink-semi.cn/wiki/IDE-and-Tools/RISC-V_IDE_for_TLSR9_Chips/

工程的建立

菜单File->import 调出如下界面:

Import界面

选择Existing Projects into Workspace->点Browse选择代码所在目录:

选择目录

点Finish 完成项目的import:

完成导入

工程的编译

菜单栏点 图标,下拉菜单选择要编译的选项即可开始编译:

开始编译

编译后生成的bin文件位置在代码目录\src\_proj_cc_tws_\output下:

bin文件路径

硬件环境

目前配套的BT/低延时双模在线通用SDK的硬件环境有通用开发板环境(B91开发板)和B91 TWS耳机套件环境。

通用开发板环境

通用开发板环境使用两块B91开发板作为TWS耳机左右耳,另一块B91开发板作为配合工作的dongle端。

通用开发板环境

B91 TWS耳机套件环境

B91 TWS耳机套件环境如下图所示,上面有屏幕模块是充当仓的板子,可以模拟仓相关的行为动作,比如开关机,出入仓,充放电等。配合工作的dongle端仍然是通用开发板。

B91 TWS耳机套件环境

软件烧录及debug

烧录

烧录软件为Telink BDT工具,下载地址如下:

Telink BDT工具

下载安装后界面如下:

BDT工具界面

下载时选择 Tool-> TWS Tool 弹出界面如下:

TWS Tool界面

下载耳机的bin:

耳机bin文件下载到0x00000000位置, 提示音下载到0x000d0000位置, 0x001dc000为耳机名字,0x001ff100为耳机地址。

下载dongle bin:

仅仅将dongle bin文件下载到0x00000000位置即可。

bin文件下载示例

Trace打印

(1) Trace配置方式

SDK所有模块的trace配置在src\tlkapi\tlk_debug.h这个枚举里定义:

定义SDK所有模块的trace

每个模块子模块trace定义在下面枚举结构体里面:

定义每个模块子模块trace

然后将每个模块trace定义的使能结构体加入到scTlkDebugInfo里面:

将使能结构体加入到scTlkDebugInfo里

例如下面这两个是system和btc模块log使能定义:

system和btc模块log使能定义

  • 1:该模块trace和VCD使能总开关
  • 2:子模块名称
  • 3:子模块trace是否使能
  • 4:子模块VCD log是否使能
  • 5:定义该子模块trace打印等级,可配置为如下几种:

定义子模块trace打印等级

Dongle端模式是USB声卡模式,不会有trace打印,如果需要使能trace模式,在dongle工程里app_config.h里宏APP_MODE定义为APP_MODE_DEBUG_ONLY即可使能trace模式。

(2) Trace打印工具

BDT工具菜单Tool->RISC-V Tool 弹出如下trace打印工具界面,TWS左右耳机同时需要打印log时,先单独上电作为右耳的耳机,此时trace可以正常打印出来,然后在3的位置设置将0xcfff0位置写入0x21,按上图红框所示内容填入后按键盘回车键生效。

trace打印工具界面

而后在BDT路径里的如下目录BDT5-7-2\BDT\config\riscv\tool中找到“tl_template.ini”文件(如下图1),相同目录下复制一份保存为“tl_template21.ini”名字,而后打开“tl_template21.ini”文件将下图中2位置内容改为红框中的内容。此时再次选择BDT工具菜单Tool->RISC-V Tool 弹出第二个trace打印界面,并在3的位置选中“tl_template21.ini”名字的文件,左耳上电后,测试的trace会显示在第一个工具界面里,右耳会显示在第二个工具界面里。

选择文件

VCD log打印

在trace工具界面选中3,在弹出对话框中选中VCD配置文件,路径是\src\common\usb_dbg\log_def_stack.h。然后耳机测试过程中,点1开始抓取VCD信息,结果会自动保存在下载路径下以.vcd保存。点2可以自动打开VCD文件供分析。

VCD log打印

TWS耳机方案基本功能操作说明

开关机操作

通用开发板环境下,耳机更新下载后或者关机后按reset键开机,使用过程中可以在RISC-V TDB下面命令行窗口输入“11 0c”触发关机,或者长按K2键超过3秒后松开触发关机。

B91 TWS耳机套件环境下,耳机更新下载后,拔插USB口上电开机,使用过程中,在双耳入仓的状态下,短按耳机基板上的绿色按键开机,双击耳机基板上的绿色按键触发关机。

耳机出入仓

B91 TWS耳机套件环境下,耳机和仓板子都处在开机状态下,耳机基板上白色按键处于按下状态表示入仓状态,处于弹起状态表示出仓状态。耳机更新程序或者是掉电再上电后,在保持出仓状态下,需要按下基板上绿色的按钮触发耳机正常识别出入仓状态。

耳机组对及配对模式

通用开发板环境下,双耳同时双击K2按键触发双耳进入配对模式,可配对dongle或者BT设备,三击K2双耳执行恢复出厂设置,进行双耳组队的动作,而后进入配对模式。

B91 TWS耳机套件环境下,耳机在入仓状态下,长按耳机基板上的绿色按键3秒后耳机进入配对模式,维持长按到10s,耳机执行恢复出厂设置,进行双耳组队的动作,而后进入配对模式。

耳机跟BT主设备和dongle建立连接

耳机在配对模式下手机等BT主设备可以搜索配对连接耳机,在耳机配对模式下,双击作为dongle的B91开发板上的K1按键可以跟耳机配对连接上。已经配对连接过手机或者dongle的耳机,正常关机后再开机可以自动回连上上次连接的手机BT和dongle。

耳机和dongle在各个模式下的灯效

Dongle端:配对模式红灯快闪,连接上红灯常亮,回连模式下红灯呼吸效果;

耳机(通用开发板环境下):配对模式红灯快闪,连接上后红灯呼吸,回连模式红灯慢闪;

耳机(B91 TWS耳机套件环境下):配对模式双灯快闪,连接上后双灯呼吸,回连模式双灯慢闪。

系统架构说明

总体架构

TWS耳机可以单独通过2.4GHz低延时私有协议连接dongle或者单独通过BT连接到主设备进行通话或者音乐播放,也可以两路连接并存进行一路电话或音乐。在两路同时在线的情况下可选择两路同时播放音乐的混音模式,或者一路BT电话加一路dongle电话或者音乐模式的混音(dongle电话只有下行有声音)。单dongle音乐模式下采用48kHz LC3编码,单dongle电话为下行48kHz LC3,上行16kHz mSBC格式, 以上两个模式下可实现低于30ms的延时播放。BT电话支持CVSD和mSBC编码格式,BT音乐支持127kHz码率的AAC和53 bitpool的SBC编码格式。

耳机软件结构

耳机软件架构

耳机软件架构如上图,各模块介绍如下:

  • PHY_RF/Driver:BT/2.4GHz RF物理层及芯片驱动相关逻辑。
  • BT_controller:BT controller底层相关逻辑。
  • Async_controller:2.4GHz低延时私有协议controller相关逻辑。
  • BT_Host/Profile:BT host层(HCI/RFCOMM/L2CAP相关),BT Profile(A2DP/HFP/AVRCP/SPP等)。
  • BT_API及Async_API:APP层和BT host层及Aasync controller层交互相关的中间层。
  • SYS_APP:处理系统应用层相关逻辑。
  • UI:处理系统所有UI判断及动作的产生。
  • Audio_Path:音频通路相关逻辑。
  • Audio_Algorithm:所有音频算法相关逻辑处理。
  • Codec:音频播放机数据采集处理。

Dongle软件结构

Dongle软件架构

Dongle端软件架构如上图,各模块介绍如下:

  • PHY_RF/Driver:2.4GHz RF物理层及芯片驱动。
  • Async_controller:2.4GHz低延时私有协议controller相关逻辑。
  • Async_API:APP层和Aasync controller层交互相关的中间层。
  • SYS_APP:处理系统应用层相关逻辑。
  • UI:处理系统所有UI判断及动作的产生。
  • Audio_Path:音频通路相关逻辑。
  • Audio_Algorithm:所有音频算法相关逻辑处理。
  • USB:USB音频相关逻辑。

软件模块说明

耳机BT API层

API层的作用是接收来自host层的事件,记录下相关的数据,并给APP层提供相应的接口调用,其位置在源码的tlkapi层目录下。

当host层上报事件到API层时,会在API层记录相关的状态供APP层使用,同时如果APP层注册了相关事件,会将此事件上报给APP层。APP层也可以通过相关接口获取API层记录的数据值。

如电话接听流程:当手机来电时,host层会对相应的hfp的指令进行解析,如果无误则将电话的来电状态上报到API层的api_hfphf_call_status_changed_callback函数中,在此函数会记录电话的call_status状态,供app使用api_bt_get_hfphf_status函数进行查询。同时,如果app层注册了此事件,api会将此事件上报给app层进行处理。

app层处理这些事件的接口,大部分在app_bt.c中实现。

API层文件位置

tlkapi_bth

tlkapi_bth_base_evt中存放的是来自host层有关ACL或SCO连接建立的基本事件,如ACL建立、断开,linkkey获取等。同时可以通过tlkapi_bth_event_user中的注册函数将相应的事件注册到APP层进行处理。

tlkapi_bth

接口说明

(1) tlkapi_bth_base_evt

数据结构:

定义api层BT host event数据结构:

  • acl_handle:记录acl handle
  • btaddr:对方设备地址
  • dev_class:对方设备类型
  • cur_role:当前角色
  • positive:主动连接或被动连接
  • cur_mode:当前模式
  • link_key:当前linkkey
  • encry_enable_flag: 加密使能标识
  • link_type:连接类型
  • sco_handle:记录sco handle
typedef struct {
    uint16_t  acl_handle;
    uint8_t   btaddr[6];
    uint32_t  dev_class;
    uint8_t   cur_role;
    uint8_t   positive;
    uint8_t   cur_mode;
    uint8_t   link_key[16];
    uint8_t   encry_enable_flag;
} api_bth_acl_base_item_t;

typedef struct {
    uint32_t   dev_class;
    uint8_t    link_type;          
    uint16_t   sco_handle;
    uint16_t   acl_handle;
    uint8_t    btaddr[6];
} api_bth_sco_base_item_t;

typedef struct {
    api_bth_acl_base_item_t acl_item[TLK_STK_BTACl_NUMB];
    api_bth_sco_base_item_t sco_item[TLK_STK_BTSCO_NUMB];
} api_bth_base_event_info_t;

接口定义:

获取api层对应handle的acl item结构体数据:

api_bth_acl_base_item_t *api_bth_evt_get_acl_item_by_handle(uint16_t handle, api_bth_base_event_info_t *p_buff)

获取api层对应address的acl item结构体数据:

api_bth_acl_base_item_t *api_bth_evt_get_acl_item_by_addr(uint8_t *addr, api_bth_base_event_info_t *p_buff)

获取api层对应handle未使用的acl item结构体数据:

api_bth_acl_base_item_t *api_bth_evt_get_acl_item_unused(api_bth_base_event_info_t *p_buff)

获取api层对应handle的sco item结构体数据:

api_bth_sco_base_item_t *api_bth_evt_get_sco_item_by_handle(uint16_t scoHandle, api_bth_base_event_info_t *p_buff)

获取api层对应address的sco item结构体数据:

api_bth_sco_base_item_t *api_bth_evt_get_sco_item_by_addr(uint8_t *addr, api_bth_base_event_info_t *p_buff)

获取api层对应handle未使用的sco item结构体数据:

api_bth_sco_base_item_t *api_bth_evt_get_sco_item_unused(api_bth_base_event_info_t *p_buff)

host回调事件: ACL connect request

int api_bth_evtid_aclconn_request_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: SCO connect request

int api_bth_evtid_scoconn_request_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: ACL connect complete

int api_bth_evtid_aclconn_complete_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: SCO connect complete

int api_bth_evtid_scoconn_complete_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: ACL disconnect complete

int api_bth_evtid_acldisc_complete_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: SCO disconnect complete

int api_bth_evtid_scodisc_complete_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: authen complete

int api_bth_evtid_authen_complete_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: encrypt complete

int api_bth_evtid_encrypt_complete_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: sco codec changed

int api_bth_evtid_scocodec_changed_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: role changed

int api_bth_evtid_role_changed_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: mode changed

int api_bth_evtid_mode_changed_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: pincode request

int api_bth_evtid_pincode_request_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: linkkey request

int api_bth_evtid_linkkey_request_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: linkkey notify

int api_bth_evtid_linkkey_notify_callback(uint8_t *p_data, uint16_t data_len)

host回调事件: ACL establish

int api_bth_evtid_acl_establish_callback(uint8_t *p_data, uint16_t data_len)

将应用层读取linkkey的事件注册到API层,用于读取应用层保存的linkkey信息。应用层事件的返回值描述:返回值为0,读取lingkkey成功,否则失败:

void api_bth_linkkey_request_register(bth_request_linkkey_func func)

将应用层删除linkkey的事件注册到API层,用于认证失败时删除应用层的linkkey信息:

void api_bth_clear_last_dev_register(bth_clear_last_dev_func func)

清除api层相关acl或sco记录的信息:

int api_bth_evt_info_clear(void *p_data, uint16_t size)

注册api层acl和sco数据:

int api_bth_evt_base_info_register(api_bth_base_event_info_t *p_data)

移除api层acl和sco数据:

int api_bth_evt_base_info_remove(void)

api层bt host相关初始化:

int api_bth_evt_init(void)

(2) tlkapi_bth_event_user

接口定义:

API层回调事件执行接口。当APP层注册相关事件后,会在API层调用此函数执行:

void api_bth_event_user_exec(uint16_t evtID, uint8_t *pData, uint16_t dataLen)

提供APP层移除API层回调事件的接口:

void api_bth_event_user_remove(uint16_t evtID)

提供APP层移除所有API层回调事件的接口:

void api_bth_event_user_clear_all(void)

提供APP层注册API层回调事件的接口:

void api_bth_event_user_register(uint16_t evtID, bth_event_func func)

tlkapi_btmisc

tlkapi_btmisc中存放的是与BT有关的杂项文件,有BT回连、scan管理、和api层数据获取等。

tlkapi_bt_get_value中实现api层对host层上报事件状态所记录数据的获取,如a2dp codec、hfp 电话状态等。

tlkapi_bt_recon实现了对回连机制的处理。

tlkapi_bt_scan实现了对scan管理的处理。

tlkapi_btmisc_event_user 可以将对应的事件注册到APP层。

所有的函数的使用方法暂时参考APP层中的调用。

tlkapi_btmisc

接口说明

(1) tlkapi_bt_get_value

接口定义:

注册API层相关变量:

void api_bt_ctrl_all_value_init(void)

通过address获取ACL handle:

uint16_t api_bt_get_acl_handle(uint8_t addr[6])

通过ACL handle获取address:

uint8_t api_bt_get_addr_by_handle(uint8_t out_addr[6], uint16_t handle)

断连对应地址的ACL链路:

void api_bt_acl_disconnect(uint8_t addr[6])

取消对应地址的ACL链路连接:

void api_bt_acl_cancel_connect(uint8_t addr[6])

获取ACL device类型:

uint32_t api_bt_get_acl_dev_class(uint8_t addr[6])

获取ACL当前角色:

uint8_t api_bt_get_acl_curr_role(uint8_t addr[6])

获取ACL positive:

uint8_t api_bt_get_acl_positive(uint8_t addr[6])

获取ACL 当前模式:

uint8_t api_bt_get_acl_curr_mode(uint8_t addr[6])

获取API记录的当前linkkey值:

uint8_t *api_bt_get_linkkey(uint8_t addr[6])

获取SCO device类型:

uint32_t api_bt_get_sco_dev_class(uint8_t addr[6])

获取SCO link type:

uint8_t api_bt_get_sco_link_type(uint8_t addr[6])

获取A2DP sink status:

BTP_A2DP_CHN_MODE_ENUM api_bt_get_a2dpsnk_status(uint8_t addr[6])

获取A2DP sink channel mode:

BTP_A2DP_CHN_MODE_ENUM api_bt_get_a2dpsnk_chn_mode(uint8_t addr[6])

获取A2DP sink codec:

uint8_t api_bt_get_a2dpsnk_codec(void)

获取avrcp音量:

uint8_t api_bt_get_avrcp_volume(uint8_t addr[6])

获取当前avrcp handle:

uint16_t api_bt_get_avrcp_handle(void)

获取hf status:

void api_bt_get_hfphf_status(uint8_t addr[6], api_hfp_call_status_t *out_buff)

获取对应地址的hf 是否存在通话:

uint8_t api_bt_get_hfphf_has_voice(uint8_t addr[6])

获取API层记录的profile连接标志:

uint16 api_bt_get_profile_connflag(uint8_t addr[6])

获取API层相应的ACL item:

api_bth_acl_base_item_t *api_bt_get_acl_item(uint8_t addr[6])

获取API层未使用的ACL item:

api_bth_acl_base_item_t *api_bt_get_unused_acl_item(void)

获取API层相应的SCO item:

uint8_t api_bt_get_has_sco_item(uint8_t addr[6])

获取A2DP sink 是否正在播放:

uint8_t api_bt_get_a2dpsnk_is_playing(uint8_t addr[6])

获取当前hf是否存在通话:

uint8_t api_bt_get_currernt_has_voice(void)

获取当前连接是否存在sco链接:

uint8_t api_bt_get_currernt_has_sco(void)

(2) tlkapi_bt_recon

宏定义:

定义回连ACL超时时间:

#define RECON_ACL_TIMEOUT        5000

定义回连流程默认超时时间:

#define API_BTREC_DEFAULT_TIME    30

定义回连流程默认超时次数:

#define API_BTREC_DEFAULT_COUNTS  2

数据结构:

定义回连过程中各状态枚举类型:

typedef enum {
    BTRECON_STATE_NONE      =0,
    BTRECON_STATE_CONNETING =1,
    BTRECON_STATE_SUCCESS   =2, BTRECON_STATE_CANCEL_BYLOCAL=3,
    BTRECON_STATE_WAIT_MASTER      =4,
    BTRECON_STATE_TIMEOUT   =5,
    BTRECON_STATE_COUNTS_MAX=6,
    BTRECON_STATE_RETRY      =7,
    BTRECON_STATE_WAITCONN_PROFILE =8, 
    BTRECON_STATE_NEW_DEV          =9,
    BTRECON_STATE_POSITIVE_FALSE   =10, 
    BTRECON_STATE_PAGE_SUCCESS     =11,
    BTRECON_STATE_MAX
} btrecon_state_e;

定义配置回连参数结构体类型:

  • con_addr[6]:回连地址

  • remote_service:需要回连的profile

typedef struct {
    uint8_t   con_addr[6]; 
    uint16_t  remote_service;  
} api_btrecon_config_t;

定义回连item结构体类型:

  • en:回连是否使能
  • state:回连状态
  • recon_info:回连配置信息
  • cur_con_cnt:当前回连次数
  • start_1s_flag:回连流程1s计时标志
  • ticks:回连ticks
  • set_con_cnt:设置的回连次数
  • set_con_time_s:设置的回连时间
  • hf_channel:hfp channel
  • try_conn_profile:已尝试回连的profile
typedef struct {
    bool   en        :1;
    btrecon_state_e   state;         
    api_btrecon_config_t  recon_info;      
    uint8_t     cur_con_cnt;
    uint16_t    start_1s_flag;   
    uint32_t    ticks;           
    uint8_t     set_con_cnt;     
    uint16_t    set_con_time_s;
    uint8_t     hf_channel;
    uint16_t    try_conn_profile;
} api_btrecon_item_t;

接口定义:

API层回连: profile connect处理函数:

void api_btrec_profile_conn_func(uint16_t acl_handle, uint8_t status, uint8_t prof_type)

API层回连: profile disconnect处理函数:

void api_btrec_profile_disconn_func(uint16_t acl_handle, uint8_t prof_type)

API层回连: 获取hf channel处理函数:

void api_btrec_sdp_set_hf_channel_func(uint16_t acl_handle, uint8_t hf_channel)

API层回连: 加密完成处理函数:

void api_btrec_encrypt_complete_func(uint16_t acl_handle)

API层回连: ACL complete处理函数:

void api_btrec_aclconn_complete_func(uint8_t addr[6], uint8_t status, uint8_t positive)

API层回连: ACL disconnect处理函数:

void api_btrec_acldisc_complete_func(uint8_t addr[6], uint8_t reason)

API层回连: 认证完成处理函数:

void api_btrec_authen_complete_func(uint8_t addr[6], uint8_t reason)

API层回连: 获取hf channel函数:

uint8_t api_btrec_get_hf_channel(void)

设置回连状态:

void api_btrec_set_state(btrecon_state_e data)

获取回连状态:

btrecon_state_e api_btrec_get_state(void)

开始回连接口:

void api_btrec_start(api_btrecon_config_t data)

停止回连接口:

void api_btrec_stop(void)

回连信息清除:

void api_btrec_clear(void)

设置回连超时时间:

void api_btrec_set_con_time(uint16_t timeout_s)

获取回连超时时间:

uint16_t api_btrec_get_con_time(void)

获取回连剩余时间:

uint16_t api_btrec_get_con_remaining_time(void)

设置回连超时次数:

void api_btrec_set_con_counts(uint8_t try_ticks)

获取回连超时次数:

uint8_t api_btrec_get_con_counts(void)

获取回连剩余次数:

uint16_t api_btrec_get_con_remaining_counts(void)

回连流程process:

void api_btrec_process(void)

(3) tlkapi_bt_scan

宏定义:

定义常开启scan时间:

#define    API_BTSCAN_FOREVER_TIME  0xFFFF

定义默认开启scan时间:

#define    API_BTSCAN_DEFAULT_TIME  (2*60)

数据结构:

定义scan流程各状态结构体类型:

typedef enum {
    BTSCAN_STATE_NONE =0,
    BTSCAN_STATE_SCANNING =1,
BTSCAN_STATE_CANCEL_BYLOCAL=2,
    BTSCAN_STATE_TIMEOUT =3,
BTSCAN_STATE_MAX
} btscan_state_e;

定义scan item结构体类型:

  • inqscan_en:inquiry scan是否使能
  • pagescan_en:page scan是否使能
  • inq_state:inquiry scan状态
  • page_state:page scan 状态
  • set_inqscan_time_s: inquiry scan设置的超时时间
  • set_pagescan_time_s: page scan设置的超时时间
  • start_inqscan_1s_flag:inquiry scan的运行时间
  • start_pagescan_1s_flag: page scan的运行时间
  • ticks: ticks标志
typedef struct {
    bool     inqscan_en    :1;
    bool     pagescan_en   :1;
    btscan_state_e     inq_state; 
    btscan_state_e     page_state;
    uint16_t  set_inqscan_time_s; 
    uint16_t  set_pagescan_time_s;
    uint16_t  start_inqscan_1s_flag; 
    uint16_t  start_pagescan_1s_flag;
    uint32_t  ticks;
} api_btscan_item_t;

接口定义:

API层scan: ACL connect完成处理函数:

void api_btscan_aclconn_complete_func(uint8_t addr[6], uint8_t status, uint8_t positive)

设置inquiry scan状态:

void api_btscan_set_inq_state(btscan_state_e data)

获取inquiry scan状态:

btscan_state_e api_btscan_get_inq_state(void)

设置inquiry scan超时时间:

void api_btscan_set_inq_time(uint16_t timeout_s)

获取inquiry scan超时时间:

uint16_t api_btscan_get_inq_time(void)

刷新inquiry scan运行时间:

void api_btscan_refresh_inq_start_tick(void)

获取inquiry scan剩余时间:

uint16_t api_btscan_get_inq_remaining_time(void)

设置page scan状态:

void api_btscan_set_page_state(btscan_state_e data)

获取page scan状态:

btscan_state_e api_btscan_get_page_state(void)

设置page scan超时时间:

void api_btscan_set_page_time(uint16_t timeout_s)

获取page scan超时时间:

uint16_t api_btscan_get_page_time(void)

刷新page scan运行时间:

void api_btscan_refresh_page_start_tick(void)

获取page scan剩余时间:

uint16_t api_btscan_get_page_remaining_time(void)

开启或关闭scan:

void api_btscan_enable(bb_scan_en_t en)

API层scan process:

void api_btscan_process(void)

(4) tlkapi_btmisc_event_user

数据结构:

定义供app层调用的bt_misc相应事件ID枚举类型:

typedef enum {
BTMISC_EVENTID_RECON_SUCCESS= 0,
BTMISC_EVENTID_RECON_CANCEL_BYLOCAL, 
BTMISC_EVENTID_RECON_WAIT_MASTER, 
BTMISC_EVENTID_RECON_TIMEOUT,
BTMISC_EVENTID_RECON_COUNTS_MAX,
BTMISC_EVENTID_RECON_RETRY,  
BTMISC_EVENTID_RECON_WAITECONN_PROFILE,               
BTMISC_EVENTID_RECON_NEW_DEVICE,
BTMISC_EVENTID_RECON_POSITIVE_FALSE,
BTMISC_EVENTID_RECON_PAGE_SUCCESS,
BTMISC_EVENTID_SCAN_INQ_CANCEL_BYLOCAL,
BTMISC_EVENTID_SCAN_PAGE_CANCEL_BYLOCAL,
BTMISC_EVENTID_SCAN_INQ_TIMEOUT,
BTMISC_EVENTID_SCAN_PAGE_TIMEOUT,
    BTMISC_EVENTID_MAX
}btmisc_eventid_e;

接口定义:

API层回调事件执行接口。当APP层注册相关事件后,会在API层调用此函数执行:

void api_btmisc_event_user_exec(btmisc_eventid_e evtID, uint8_t *pData, uint16_t dataLen)

提供APP层移除API层回调事件的接口:

void api_btmisc_event_user_remove(btmisc_eventid_e evtID)

提供APP层移除所有API层回调事件的接口:

void api_btmisc_event_user_clear_all(void)

提供APP层注册API层回调事件的接口:

void api_btmisc_event_user_register(btmisc_eventid_e evtID, btmisc_event_func func)

tlkapi_btp

tlkapi_btp中存放的是来自host层有关profile的事件,可以通过tlkapi_btp_event_user中的注册函数将相应的事件注册到APP层进行处理。

其中tlkapi_profile_evt中存放了profile的连接、断连等事件。根据profile的类型将不同的profile事件的处理函数定义在不同的文件中,以文件名称体现。如hand-free协议中相关电话的处理函数均定义在tlkapi_hfp文件中,当电话的音量发生改变时(作为hf角色时),host的事件会调用到此文件中的api_hfphf_volume_changed_callback函数进行处理。

tlkapi_btp

接口说明

(1) tlkapi_a2dp

数据结构:

定义A2DP参数结构体类型:

  • acl_handle: acl handle
  • status: A2DP 状态
  • chn_mode: channel mode
  • frequence:A2DP 采样率
  • aac_obj_type: AAC object type
  • aac_bit_rate: bit rate
typedef struct {
    uint16_t               acl_handle;
    BTP_A2DP_STATUS_ENUM   status;
BTP_A2DP_CHN_MODE_ENUM chn_mode;

    BTP_A2DP_CODEC_ENUM  codec;
    uint16_t               frequence;
    uint8_t                aac_obj_type;
    uint32_t               aac_bit_rate; 
} api_a2dp_base_item_t;

typedef struct {
    api_a2dp_base_item_t item[TLK_BT_A2DP_MAX_NUMB];
} api_a2dp_base_info_t;

接口定义:

通过handle获取a2dp item:

api_a2dp_base_item_t *api_a2dp_get_item_by_handle(uint16_t handle, api_a2dp_base_info_t *p_buff)

a2dp事件回调: source codec changed

int api_a2dpsrc_codec_changed_callback(uint8_t *p_data, uint16_t data_len)

a2dp事件回调: sink codec changed

int api_a2dpsnk_codec_changed_callback(uint8_t *p_data, uint16_t data_len)

a2dp事件回调: source status changed

int api_a2dpsrc_status_changed_callback(uint8_t *p_data, uint16_t data_len)

a2dp事件回调: sink status changed

int api_a2dpsnk_status_changed_callback(uint8_t *p_data, uint16_t data_len)

清除a2dp item结构体数据:

int api_a2dp_base_info_clear(void *p_data, uint16_t size)

注册a2dp item接口:

int api_a2dp_base_info_register(api_a2dp_base_info_t *src, api_a2dp_base_info_t *snk)

移除a2dp item接口:

int api_a2dp_base_info_remove(bool src, bool snk)

a2dp初始化函数:

int api_btp_a2dp_evt_init(void)

(2) tlkapi_avrcp

数据结构:

定义AVRCP参数结构体类型:

  • acl_handle: ACL handle
  • cur_volume: 当前音量
typedef struct {
    uint16_t acl_handle;
    uint8_t  cur_volume;
}api_avrcp_base_item_t;

typedef struct{
    api_avrcp_base_item_t item[TLK_BT_AVRCP_MAX_NUMB];
}api_avrcp_base_info_t;

接口定义:

通过handle获取avrcp item:

api_avrcp_base_item_t *api_avrcp_get_item_by_handle(uint16_t handle, api_avrcp_base_info_t *p_buff)

avrcp事件回调: volume change

void api_avrcp_change_volume_callback(uint16_t acl_handle, uint8_t volume)

设置当前音量到host层:

void api_avrcp_set_host_note_cur_music_vol(uint16_t handle, uint8_t vol_perc)

清除avrcp item结构体数据:

int api_avrcp_baseInfo_clear(void *p_data, uint16_t size)

注册avrcp item接口:

int api_avrcp_baseInfo_register(api_avrcp_base_info_t *avrcp_info)

移除avrcp item接口:

int api_avrcp_baseInfo_remove(void)

avrcp初始化函数:

int api_btp_avrcp_evt_init(void)

(3) tlkapi_btp_event_user

接口定义:

API层回调事件执行接口。当APP层注册相关事件后,会在API层调用此函数执行:

void api_btp_event_user_exec(uint16_t evtID, uint8_t *pData, uint16_t dataLen)

提供APP层移除API层回调事件的接口:

void api_btp_event_user_remove(uint16_t evtID)

提供APP层移除所有API层回调事件的接口:

void api_btp_event_user_clear_all(void)

提供APP层注册API层回调事件的接口:

void api_btp_event_user_register(uint16_t evtID, btp_event_func func)

(4) tlkapi_hfp

数据结构:

定义call setup 枚举类型:

typedef enum _hshf_callsetup_enum{
    CALL_SETUP_NONE          = 0,
    CALL_SETUP_INCOMING     = 1,
    CALL_SETUP_OUTGOING     = 2,
    CALL_SETUP_REMOTE_ALERT = 3,
    CALL_SETUP_ESTABLISHED   = 4
}hfp_callsetup_status_e;

定义call status枚举类型:

typedef enum _hshf_call_enum{
CALL_NONE                = 0,
CALL_ESTABLISHED         = 1
}hfp_call_status_e;

定义call status结构体类型:

  • status: call status
  • status_setup: call setup
  • status_held: call held
typedef struct {
    uint8_t  status;
    uint8_t  status_setup;
    uint8_t  status_held;
}api_hfp_call_status_t;

定义HFP参数结构体类型:

  • acl_handle: ACL handle
  • codec: hfp codec
  • call: call status
typedef struct {
    uint16_t acl_handle;
    uint8_t  codec;
    api_hfp_call_status_t call;

}api_hfp_base_item_t;

typedef struct{
    api_hfp_base_item_t item[TLK_BT_HFP_MAX_NUMB];
}api_hfp_base_info_t;

接口定义:

通过handle获取hfp item:

api_hfp_base_item_t *api_hf_get_item_by_handle(uint8_t handle, api_hfp_base_info_t *p_buff)

hfp事件回调: hf codec changed

int api_hfphf_codec_changed_callback(uint8_t *p_data, uint16_t data_len)

hfp事件回调: hf status changed

int api_hfphf_status_changed_callback(uint8_t *p_data, uint16_t data_len)

hfp事件回调: hf call status changed

int api_hfphf_call_status_changed_callback(uint8_t *p_data, uint16_t data_len)

hfp事件回调: hf volume changed

int api_hfphf_volume_changed_callback(uint8_t *p_data, uint16_t data_len)

发送电池电量等级接口:

int api_hfphf_send_battery_level(uint8_t batt_level)

清除hfp item结构体数据:

int api_hfp_baseInfo_clear(void *p_data, uint16_t size)

注册hfp item接口:

int api_hfp_baseInfo_register(api_hfp_base_info_t *hf, api_hfp_base_info_t *ag)

移除hfp item接口:

int api_hfp_baseInfo_remove(bool hf, bool ag)

hfp初始化函数:

int api_btp_hfp_evt_init(void)

注册spp接收数据callback:

void api_btp_spp_RecvDataCB_register(api_btp_spp_rx_event_t func)

(5) tlkapi_profile_evt

数据结构:

定义profile连接参数结构体类型:

  • acl_handle:ACL handle
  • btaddr[6]: 连接地址
  • conn_flag: 连接的profile协议
  • remote_services:remote services
typedef struct {
    uint16_t  acl_handle;
    uint8_t   btaddr[6];
    uint16_t  conn_flag;
    uint8_t   remote_services;
}api_profile_item_t;

typedef struct{
    api_profile_item_t item[TLK_STK_BTACl_NUMB];
}api_profile_base_info_t;

接口定义:

通过handle获取profile item:

api_profile_item_t *api_profile_get_item_by_handle(uint16_t handle, api_profile_base_info_t *p_buff)

获取未使用的profile item:

api_profile_item_t *api_profile_get_item_unused(api_profile_base_info_t *p_buff)

profile事件回调: profile request

int api_btp_evtid_profile_request_callback(uint8_t *p_data, uint16_t data_len)

profile事件回调: profile connect

int api_btp_evtid_profile_connect_callback(uint8_t *p_data, uint16_t data_len)

profile事件回调: profile disconnect

int api_btp_evtid_profile_disconn_callback(uint8_t *p_data, uint16_t data_len)

profile事件回调: profile service

int api_btp_evtid_profile_service_callback(uint8_t *p_data, uint16_t data_len)

profile事件回调: profile channel

int api_btp_evtid_profile_channel_callback(uint8_t *p_data, uint16_t data_len)

清除profile item结构体数据:

int api_profile_baseInfo_clear(void)

注册profile item接口:

int api_profile_baseInfo_register(api_profile_base_info_t *profile_info)

移除profile item接口:

int api_profile_baseInfo_remove(void)

profile初始化函数:

int api_btp_profile_evt_init(void)  

tlkapi_flash

tlkapi_nvds中存放的是有关flash的初始化,读写、删除等函数,对flash的区间进行了划分,具体使用可参看APP层的app_flash.c等文件。

tlkapi_flash

(1) tlkapi_nvds

宏定义:

定义默认flash页大小:

#ifndef FLASH_PAGE_SIZE
#define FLASH_PAGE_SIZE  256
#endif

定义默认flash扇区大小:

#ifndef FLASH_SECTOR_SIZE
#define FLASH_SECTOR_SIZE    4096
#endif

定义默认使用的两个flash块地址:

#ifndef FLASH_SEC_0_ADDR
#define FLASH_SEC_0_ADDR    0xea000
#endif
#ifndef FLASH_SEC_1_ADDR
#define FLASH_SEC_1_ADDR    0xeb000
#endif

定义NVDS存储单元大小:

#ifndef NVDS_STORE_UINT_SIZE
#define NVDS_STORE_UINT_SIZE  64
#endif

定义默认可连接最大BT设备数量:

#ifndef MAX_PAIRED_DEVICE_NUM
#define MAX_PAIRED_DEVICE_NUM  5
#endif

定义默认可连接最大dongle设备数量:

#ifndef MAX_PAIRED_DONGLE_NUM
#define MAX_PAIRED_DONGLE_NUM  1
#endif

定义NVDS存储块数量:

#define NVDS_STORE_NUMBE (FLASH_SECTOR_SIZE / NVDS_STORE_UINT_SIZE)

定义NVDS有效帧头:

#define NVDS_SECTOR_VALID_MAGIC  0xcafecafe

定义NVDS偏移:

#define TAG_OFFSET_INVALID   0

数据结构:

定义NVDS错误代码返回值:

typedef enum
{
    NVDS_ERR_SUCCESS = 0,
    NVDS_ERR_INVALID_STATE,
    NVDS_ERR_INVALID_PARA,
    NVDS_ERR_INVALID_LEN,
    NVDS_ERR_DATA_CORRUPT,
    NVDS_ERR_INVALID_SECTION,
}nvds_err_e;

定义NVDS存储数据TAG ID标志:

typedef enum
{
#if !PROJ_CC_DONGLE
    NVDS_TAG_BT_GLOBAL = 0,
    NVDS_TAG_DFU_IMG_INFO,
    NVDS_TAG_BT_BONDED_0,
    NVDS_TAG_BT_BONDED_MAX_NUM = NVDS_TAG_BT_BONDED_0 + (MAX_PAIRED_DEVICE_NUM - 1),

    NVDS_TAG_2P4G_GLOBAL_CFG,
    NVDS_TAG_DONGLE_0,
    NVDS_TAG_DONGLE_MAX_NUM = NVDS_TAG_DONGLE_0 +  (MAX_PAIRED_DONGLE_NUM - 1),

    NVDS_TAG_APP_CFG,
    NVDS_TAG_MAX,
#else //dongle
    NVDS_TAG_DFU_IMG_INFO = 0,
    NVDS_TAG_CONIFG_NAME,
    NVDS_TAG_MAX,
#endif
}nvds_tag_id_t;

定义NVDS头类型:

typedef struct
{
    uint32_t magic;
    uint32_t erase_cnt;
}__attribute__((packed)) sector_header_t;

定义NVDS tag头类型:

typedef struct
{
    uint8_t  empty      : 1;
    uint8_t  valid       : 1;
    uint8_t  rsv_bits     : 6;
    uint8_t  tag;
    uint8_t  crc;
    uint8_t  len;
}__attribute__((packed)) tag_header_t;

定义nvds数据结构体:

typedef struct
{
    uint32_t chip_id;
    uint32_t sector_addr;
    uint32_t erase_cnt;
    uint8_t  next_store_unit;
    uint8_t  tag_offset[NVDS_TAG_MAX];
}nvds_t;

定义flash保护结构体类型:

typedef struct
{
    uint32_t addr;
    uint8_t  bp;
    uint8_t  cmp;
}flash_protect_setting_t;

接口定义:

获取flash的capacity id:

uint8_t capacity_id(void)

nvds数据复位:

void api_nvds_reset(void)   

nvds初始化:

nvds_err_e api_nvds_init(void)  

移除扇区数据:

nvds_err_e api_nvds_sector_move(void)   

nvds写数据:

nvds_err_e api_nvds_write(nvds_tag_id_t tag_id, void* data, uint16_t size)  

nvds读数据:

nvds_err_e api_nvds_read(nvds_tag_id_t tag_id, void* data, uint16_t size)   

nvds删除相关数据:

nvds_err_e api_nvds_delete(nvds_tag_id_t tag_id)    

获取flash大小:

uint32_t api_nvds_flash_size(void)  

开启、关闭flash保护接口:

nvds_err_e api_nvds_flash_protect(uint32_t size)    

tlkapi_key

tlkapi_key中存放的是关于按键相关的函数,如按键的添加、删除,按键扫描、处理函数,mode机制、table参数表的注册等,具体函数可参考头文件中的声明,使用方法见第三节APP层的介绍。

tlkapi_key_bt_func中存放的是有关BT音乐或电话控制的函数,如上一曲、下一曲,接听、挂断等函数接口,此函数会在app层进行二次封装,即app层判断相关状态,从而选择调用哪一个方法。

tlkapi_key

Interface description:

(1) tlkapi_key

宏定义:

定义默认按键数量:

#ifndef API_KEY_NUM_MAX
#define API_KEY_NUM_MAX       1
#endif

定义默认按键有效电平:

#ifndef KEY_VALID_LEVEL
#define KEY_VALID_LEVEL       1
#endif

定义最小长按时间(单位10ms):

#define  MIN_LONG_CNT         100

定义最小保持时间(单位10ms):

#define  MIN_HOLD_CNT         100

定义最小超长按时间(单位10ms):

#define  MIN_LONG_LONG_CNT  400

定义判定非连续点击最小间隔时间(单位10ms):

#define  MIN_INTERVEL         10

数据结构:

定义按键状态枚举类型:

typedef enum {
    KEY_DOWN = 0, 
    KEY_HOLD = 1, 
    KEY_LONG = 2, 
    KEY_RELEASE = 3
} key_state_e;

定义按键事件:

typedef enum {
    KEY_SHORT_EVT    = 0, 
    KEY_DCLICK_EVT   = 1, 
    KEY_TCLICK_EVT   = 2, 
    KEY_HOLD_EVT     = 3,
    KEY_LONG_EVT     = 4,
    KEY_LONGLONG_EVT = 5,
    KEY_EVT_FLAG_MAX
} key_evt_flag_e;

定义按键事件flag标志:

typedef union {
    uint32_t _flg; /**< 32bit */

    struct {
        /**< byte 0 */
        uint8_t _short    :1; /**< 0 */
        uint8_t dclick    :1; /**< 1 */
        uint8_t tclick     :1; /**< 2 */
        uint8_t hold      :1; /**< 3 */
        uint8_t _long     :1; /**< 4 */
        uint8_t long_long  :1; /**< 5 */
        uint8_t res       :2;
    /**< byte 1~3 */
    };
} key_event_flg_u;

定义按键事件类:

typedef struct {
    uint8_t  _short;
    uint8_t  dclick; 
    uint8_t  tclick; 
    uint8_t  hold; 
    uint8_t  _long;
    uint8_t  longlong; 
} key_event_class_t;

定义事件处理结构体类型:

typedef struct {
    key_event_flg_u   evt_occurr;
    key_event_class_t  mode;
} key_event_t;

定义按键配置结构体类型:

typedef struct {
    API_GPIO_TYPE_DEF key_pin;
    API_GPIO_TYPE_DEF key_out_pin; 
    uint8_t       key_down_level;
    uint32_t      key_hold_cnt; 
    uint32_t      key_long_cnt; 
    uint32_t      key_long_long_cnt;
    uint32_t      key_intervel_cnt; 
} key_config_t;

定义按键mode事件表:

typedef struct {
    uint16_t  table_count;
    key_func *p_key_table;
} key_event_table_t;

定义各类事件回调集合:

typedef struct {
    key_func  _short;
    key_func  dclick;
    key_func  tclick; 
    key_func  hold; 
    key_func  _long; 
    key_func  longlong; 
} key_event_extra_t;

定义按键控制信息类型:

typedef struct {
    uint8_t       key_id;
    uint32_t      key_down_cnt;
    uint32_t      key_up_cnt; 
    uint32_t      key_click_cnt;
    key_state_e   key_state;
    key_config_t  key_info; 
} key_ctl_info_t;

接口定义:

通过GPIO获取按键ID:

uint8_t api_get_key_id_by_gpio(API_GPIO_TYPE_DEF gpio, API_GPIO_TYPE_DEF out_gpio)

添加按键:

uint8_t api_key_add(key_config_t *p)

删除按键:

uint8_t api_key_remove(uint8_t key_id)

注册按键mode事件表:

uint8_t api_key_evt_table_register(const key_func *table, uint16_t tbl_mode_num)

移除按键mode事件表:

uint8_t api_key_evt_table_remove(void)

注册事件回调:

uint8_t api_key_evt_happen_register(uint8_t key_id, key_event_extra_t *p_event)

移除事件回调:

uint8_t api_key_evt_happen_remove(uint8_t key_id)

判定是否执行按键扫描函数的注册:

uint8_t api_key_scan_not_exec_cb_register(key_func p_event)

通过mode获取事件回调:

uint8_t api_get_key_evt_by_mode(uint16_t mode, key_func* out_func)

执行mode相应的事件回调:

uint8_t api_key_evt_func_execute(uint16_t mode_index)

设置按键当前mode:

uint8_t api_set_key_curr_evt_mode(uint8_t key_id, key_event_class_t *new_mode)

获取按键当前mode:

uint8_t api_get_key_curr_evt_mode(uint8_t key_id, key_event_class_t *out_mode)

按键扫描函数:

void api_key_scan(void)

按键process:

void api_key_process(void)

(2) tlkapi_key_bt_func

接口定义:

BT电话接听:

void api_key_func_bt_call_answer(void)

BT电话挂断:

void api_key_func_bt_call_end(void)

BT电话拒接:

void api_key_func_bt_reject_call(void)

BT接受第三方来电通话并挂起现有通话

void api_key_func_accept_wait_hold_active(void)

BT第三方通话挂断并唤醒等待:

void api_key_func_end_active_resume_hold(void)

BT音乐下一曲:

void api_key_func_bt_music_forward(void)

BT音乐上一曲:

void api_key_func_bt_music_backward(void)

BT音乐播放暂停:

void api_key_func_bt_music_play_pause(void)

BT语音助手程序:

void api_key_func_trigger_voice_assistant(void)

BT siri处理函数:

void api_key_hfp_iphone_siri_handler(void)

BT三方通话拒接并保持现有通话:

void api_key_func_bt_reject_wait_keep_active(void)

tlkapi_led

tlkapi_led中存放的是关于LED灯效相关的函数,如灯的添加、删除, 灯效的设置、插入等,具体函数可参考头文件中的声明,使用方法见第三节APP层的介绍。

tlkapi_led

接口说明:

(1) tlkapi_led

宏定义:

定义LED灯效最大时间:

#define TIME_FOREVER    0xffff

定义LED灯效最小时间:

#define TIME_NO_TIME    0x0000

定义LED常闪烁:

#define FLASH_FOREVER   0xffff

定义未设置颜色标识:

#define LED_COLOR_NOT_SET   0xffff

定义最大颜色占空比:

#define LED_MAX_COLOR       BRT_HIGHT_LIGHT_FULL_DUTY

定义默认颜色:

#define LED_DEFAULE_COLOR (LED_MAX_COLOR)

数据结构:

定义设置灯效的参数来源:

typedef enum {
    DEFAULT_PATTERN = 0,
    USER_PATTERN = !DEFAULT_PATTERN
} led_patt_from_e

定义灯效参数table索引:

typedef enum {
    LED_PATTERN_LED_OFF = 0,
    LED_PATTERN_LED_ON = 1,
    LED_PATTERN_LED_FLASH_SLOW = 2,
    LED_PATTERN_LED_FLASH_FAST = 3,
    LED_PATTERN_LED_FLASH_PAIR = 4,
    LED_PATTERN_LED_IDLE = 5,
    LED_PATTERN_LED_FAST_FLASH_1_TIME = 6,
    LED_PATTERN_LED_FAST_FLASH_2_TIMES = 7,
    LED_PATTERN_LED_SLOW_FLASH_2_TIMES = 8,
    LED_PATTERN_LED_FAST_FLASH_3_TIMES = 9,
    LED_PATTERN_LED_ON_500MS = 10,
    LED_PATTERN_LED_ON_1S = 11,
    LED_PATTERN_LED_SLOW_FLASH_3_TIMES = 12,
    LED_PATTERN_LED_BREATH_ON_OFF = 13,
    LED_PATTERN_LED_BREATH_ON = 14,
    LED_PATTERN_LED_BREATH_OFF = 15,
    LED_PATTERN_LED_BREATH_ONE_TIME = 16,
    LED_PATTERN_LED_BREATH_TWO_TIMES = 17,
    LED_PATTERN_LED_BREATH_THREE_TIMES = 18,
    LED_PATTERN_LED_BREATH = 19,
    LED_PATTERN_LED_BREATH_PAIR = 20,
    LED_PATTERN_LED_HIGH_LIGHT_FLASH_1_TIME = 21,
    LED_PATTERN_LED_MAX_STATE,
} led_pattern_index_e;

定义灯效配置结构类型:

  • led_pin: LED gpio
  • led_on_level: LED有效电平
  • pwmid: 使用的pwm id
typedef struct {
    API_GPIO_TYPE_DEF  led_pin;
    uint8_t            led_on_level;
    pwm_id_e           pwmid;
} led_config_t;

定义LED设置参数类型:

  • behavior: LED行为,闪烁或呼吸灯
  • duty:占空比
  • latency: 延迟时间
  • flash_times: 闪烁次数
  • keep_high: 高位保持时间
  • keep_low:低位保持时间
typedef struct {
    uint16_t behavior; 
    uint16_t duty; 
    uint16_t latency; 
    uint16_t flash_times; 
    uint16_t keep_high; 
    uint16_t keep_low;
} led_pattern_t;

定义LED状态结构体:

  • state: 当前运行状态
  • flags: LED 相关flag
  • lum: 当前运行step记录
  • cnt: 运行次数
  • ref: 时间计数标志
typedef struct {
    uint16_t state;
    uint16_t flags;
    uint16_t lum;
    uint16_t cnt;
    uint32_t ref;
} lum_state_t;

定义LED参数table结构体类型:

typedef struct {
    uint16_t      pattern_count;
    led_pattern_t  *p_pattern_table;
} led_pattern_table_t;

定义LED颜色相关设置:

typedef struct {
    uint16_t color_value[API_LED_NUM_MAX];
} led_color_t;
typedef struct {
    uint16_t     color_count;
    led_color_t   *p_color_table;
} led_color_table_t;

定义LED行为相关结构体:

typedef struct {
    led_pattern_t  *p_pattern;
    lum_state_t   lum_state;
    led_pattern_t  *p_insert_pattern; 
    lum_state_t   insert_lum_state;
} led_env_t;

定义LED控制信息:

typedef struct {
    uint8_t       led_id;
    uint16_t      color_duty;
    led_env_t     led_env;
    led_config_t   led_info; 
} led_ctl_info_t;

接口定义:

LED添加函数:

uint8_t api_led_add(led_config_t *p)

LED添加移除:

uint8_t api_led_remove(uint8_t led_id)

通过GPIO获取LED id:

uint8_t api_get_led_id_by_gpio(API_GPIO_TYPE_DEF gpio)

注册LED用户参数table表:

uint8_t api_led_user_patterns_register(led_pattern_table_t *table, uint16_t tbl_mode_num)

移除LED用户参数table表:

uint8_t api_led_user_patterns_remove(void)

设置LED颜色duty:

void api_set_led_color_duty(led_color_t *color)

获取LED颜色duty:

void api_get_led_color_duty(led_color_t *out_color)

获取LED参数duty:

uint16_t api_led_get_pattern_duty(int led_id)

获取LED当前lum值:

uint16_t api_led_get_current_lum(int led_id)

设置LED灯效:

uint8_t api_led_pattern_set(int led_id, int patt, led_patt_from_e is_user_patt)

插入LED灯效:

uint8_t api_led_pattern_insert(int led_id, int patt, led_patt_from_e is_user_patt)

判定LED当前是否运行的是插入灯效:

uint8_t api_led_is_insert_pattern_running(int led_id)

LED刷新process:

void api_led_refresh_process(void)

耳机UI及app层功能说明

LED功能

(1) LED添加与删除

通过调用API层的函数api_led_add和api_led_remove对LED灯进行添加与删除。其中,添加函数的参数为 led_config_t *类型的结构体指针,类型定义如下:

typedef struct {
    API_GPIO_TYPE_DEF led_pin;
    uint8_t     led_on_level; /**< 0 or 1    default:LED_VALID_LEVEL   */
    pwm_id_e  pwmid;      /**< io pwm */
} led_config_t;

led_pin为LED连接到芯片的IO引脚。

led_on_level为此LED是高电平点亮还是低电平点亮。 led_on_level为LED点亮的电平,可设置为高电平或者低电平。

pwmid为此LED所使用的pwm id,如果芯片引脚是对应固定的pwm id,则此芯片led_config_t结构体中没有此域。

在demo程序中,有一个名称为g_led_state的全局变量,其有一个led_id的域,此值是为了方便控制app层的灯,初始化时可将对应的索引存放对应的ID,如蓝灯索引LED_INDEX_BLUE定义为1,g_led_state.led_id[LED_INDEX_BLUE]所表示的为蓝灯所对应灯的ID,当然此ID也可以通过IO口使用API层的api_get_led_id_by_gpio函数进行查询。

另外,本SDK提供的LED部分支持呼吸灯的特效,所以在使用api中的LED添加函数时,LED对应的引脚要支持pwm,否则可能初始化失败。

在LED添加完成后,需要将api_led_refresh_process添加到main循环中进行LED的pwm刷新。将app层中的LED任务同样添加到main循环中进行状态的更新。

(2) LED状态设置

LED通过调用API层的函数api_led_pattern_set进行设置当前灯效的状态。使用api_led_pattern_insert进行插入一个灯效,其执行优先级高于api_led_pattern_set函数,执行完成后恢复到之前set的状态。因此,在使用insert插入的时候,避免使用forever性质的灯效参数。API层这两个函数对所有初始化过的LED均有效。

如:更改状态为常亮,使用api_led_pattern_set。之后使用api_led_pattern_insert又插入闪烁1次,此时灯效进行一次闪烁之后,恢复到常亮状态。

在demo程序中,又封装了三个函数接口:

uint8_t led_get_id(uint8_t index_flag);
void led_set_all_ctrl_pattern(int patt, led_patt_from_e is_user_patt);
void led_insert_all_ctrl_pattern(int patt, led_patt_from_e is_user_patt);

其中,第一个函数是获取上一小节提到的添加到g_led_state.led_id中的ID,方便实现app层的灯效控制。另外两个函数是对已经添加到g_led_state.led_id中的LED统一设置相同的状态。

无论API层还是APP层中封装的两个LED设置的函数,均使用到了灯效的参数类型led_patt_from_e,详细见下一小节。

(3) LED参数表的设置

在API层的两个LED设置函数中(APP层同理):

uint8_t api_led_pattern_set(int led_id, int patt, led_patt_from_e is_user_patt);
uint8_t api_led_pattern_insert(int led_id,
                         int patt,
                         led_patt_from_e is_user_patt);

两者均使用到了参数patt(refer to 'led_pattern_t')和参数is_user_patt(refer to 'led_patt_from_e'),其两个参数类型如下:

typedef struct {
    uint16_t behavior;  /**< zero is flash, non-zero is breath       */
    uint16_t duty;      /**< Duty for LED on                  */
    uint16_t latency;    /**< Off time                         */
    uint16_t flash_times; /**< Breath or flash counter              */
    uint16_t keep_high;  /**< unit is mS                        */
    uint16_t keep_low;  /**< unit is mS                        */
} led_pattern_t;

typedef enum {
    DEFAULT_PATTERN = 0, 
    USER_PATTERN = !DEFAULT_PATTERN
} led_patt_from_e;

参数is_user_patt的值为DEFAULT_PATTERN时,参数patt为API默认提供的参数表的索引值;参数is_user_patt的值为USER_PATTERN,参数patt为用户所注册的参数表的索引值;

参数表是类型为led_pattern_t的数组,使用api_led_user_patterns_register对用户提供的参数进行注册,此函数需要两个参数,即参数表首地址和参数的个数。使用户可用api_led_user_patterns_remove对用户注册的参数表进行删除。用户所写的参数表进行注册后即可使用USER_PATTERN对灯效进行设置。

API提供的注册表的索引值见‘led_pattern_index_e’,其中常用的值为:

灯效关闭:LED_PATTERN_LED_OFF

灯效常亮:LED_PATTERN_LED_ON

呼吸效果:LED_PATTERN_LED_BREATH

灯效快闪:LED_PATTERN_LED_FLASH_FAST

灯效慢闪:LED_PATTERN_LED_FLASH_SLOW

(4) LED同步

TWS的demo程序中,当主从耳机连接上时,主耳灯效变化的同时需要将此状态同步给从耳,在APP层LED的任务中调用了led_state_sync函数可对此时的LED状态进行同步。同时,在主从耳机的灯效为呼吸灯时,由于主从耳机main_loop的执行效率不同,可能会出现长时间后,两个耳机的灯效不同步的问题,所以在LED任务中会检测上次灯效变化的时间,若长时间没有同步灯效,则会主动进行一次灯效的同步。其使用的函数为led_set_resync_auto_ticks(TRUE),最终调用的灯同步的函数依旧是led_state_sync。

(5) Demo流程

上电开机时,有1s的灯效闪烁,随后如果存在低电量检测的code,会先进行低电检测,检测完成后主耳(或单耳)会根据状态对灯效进行设置,并同步给从耳。

具体流程如下:

(a) 开机上电初始化

上电后,在user_init()函数中调用LED的初始化函数led_init()或led_evb_init()。配置相应的控制io、控制灯亮的有效电平、对应所使用的pwmid(如果有pwmid域),并调用API层函数api_led_add()注册添加,此时把相应的api_led_refresh_process()函数添加到主循环中,便可以进行控制LED闪烁。demo程序将api_led_refresh_process()函数添加到了app_led_task()中进行轮询执行。

在初始化函数中,可根据需要添加相关的程序进行记录状态等。如led_init()中,将相应的led_id添加到全局变量g_led_state.led_id中进行记录,方便后续调用函数led_set_all_ctrl_pattern()对所有的LED统一进行控制。

(b) 设置开机灯效

demo中在LED初始化的最后,调用了led_set_power_on_state()设置开机闪烁1s的灯效。在app_led_task()函数中,有相应的标志led_get_power_on_keep_flag(),在此期间不会进行系统状态的判断去改变灯效。

(c) 开机低电灯效闪烁

如果代码使能了LED低电闪烁的宏定义POWERON_CHECK_LOWPOWER,则会在开机灯效过后,对此时是否低电进行检查。如果是低电状态,会优先闪烁低电灯效,双耳模式下主耳会等待从耳的检测流程。

(d) 系统状态检测与同步

在app_led_task()中,执行完以上步骤,主耳或者单耳会根据系统的状态去控制灯效的改变,且主耳改变灯效时会将现在的状态发送给从耳实现主从耳机灯效的同步。当系统状态长时间没有改变时,为了使主从耳机两边灯效保持同步,主耳会每隔一段时间同步一次灯效。

现系统有以下状态:

SYS_STATE_CONNECTED:默认呼吸灯效。

SYS_STATE_IDLE:默认缓慢闪烁。

SYS_STATE_PAIR:默认快速闪烁。

SYS_STATE_POWER_OFF:关机状态,无设置。

(e) 其它灯效

其它灯效如入仓灯效,低电灯效等,均可调用api_led_pattern_insert()或led_insert_all_ctrl_pattern()此类插入性质的灯效执行,执行完后恢复到系统状态相应的灯效。

Key功能

(1) Key添加与删除

key通过调用API层的函数api_key_add和api_key_remove对按键key进行添加与删除。其中,添加函数的参数为 key_config_t *类型的结构体指针,类型定义如下:

typedef struct {
    API_GPIO_TYPE_DEF key_pin;
    API_GPIO_TYPE_DEF key_out_pin;
    unsigned char     key_down_level;
    unsigned int      key_hold_cnt;
    unsigned int      key_long_cnt;
    unsigned int      key_long_long_cnt;
    unsigned int      key_intervel_cnt;
} key_config_t;

key_pin:为key连接到芯片的IO引脚。

key_out_pin:如果是矩阵扫描的接法,此值为另外一端的IO引脚,否则必须为0。

key_down_level:按键按下时的电平状态。

key_hold_cnt:按键保持事件判断时间,单位10ms,最小值MIN_HOLD_CNT。

key_long_cnt:按键长按事件判断时间,单位10ms,最小值MIN_LONG_CNT。

key_long_long_cnt:按键超长按时间,单位10ms,最小值MIN_LONG_LONG_CNT。

key_intervel_cnt:按键多击判断时间,单位10ms,最小值MIN_INTERVEL。

其中保持事件和长按、超长按事件不共存,即当key_hold_cnt为0时才会触发长按和超长按事件。所有的时间小于最小时间时均被设为默认值。

添加完成后,将按键扫描函数api_key_scan加入到main循环中进行轮询,即可检测到按键的事件触发,包含单击、双击、三击、长按、超长按或者保持事件。

同LED,按键注册的key id可以通过api_get_key_id_by_gpio函数进行获取,此函数的两个参数为key_pin和key_out_pin。

(2) Key事件处理函数

API提供了key按键事件处理的接口:

uint8_t api_key_evt_happen_register(uint8_t key_id, 
key_event_extra_t *p_event);
uint8_t api_key_evt_happen_remove(uint8_t key_id);

register函数用来注册,key_id可以用io来获取,p_event为此id下分别对应各个事件的回调函数,其类型为key_event_extra_t:

typedef struct {
    key_func _short;   /**<  short key event extra                */
    key_func dclick;   /**<  double click event extra             */
    key_func tclick;   /**<  triple click event extra             */
    key_func hold;     /**<  hold key event extra                 */
    key_func _long;    /**<  long key event extra                 */
    key_func longlong; /**<  long long key event extra            */
} key_event_extra_t;

使用注册函数完成注册后,将api_key_process按键程序放入main循环中进行处理即可。

api_key_evt_happen_remove函数用来移除此id下的事件。

(3) Key mode机制

Key mode机制旨在设计一个灵活的key按键响应模式,基本原理是当按键事件发生时,在其注册的happen回调函数中,对其所处的状态进行不同的mode事件处理,实现一个可配置的按键功能。API层提供了mode事件的参数表的注册与删除,和对应mode回调的设置、获取与执行,如下:

uint8_t api_key_evt_table_register(const key_func *table, uint16_t tbl_mode_num);
uint8_t api_key_evt_table_remove(void);
uint8_t api_set_key_curr_evt_mode(uint8_t key_id, key_event_class_t *new_mode);
uint8_t api_get_key_curr_evt_mode(uint8_t key_id, key_event_class_t *out_mode);
uint8_t api_key_evt_func_execute(uint16_t mode_index);

table:即各场景函数的集合,如demo程序中的上一曲、下一曲等。

tbl_mode_num:table表的参数个数。

new_mode:设置keyid所设置的各个按键的mode索引值。

out_mode:获取keyid所设置的各个按键的mode索引值。

mode_index:获取此索引值对应的按键回调方法。

在demo中,table表为s_key_evt_table,其所对应的索引值为枚举key_evt_mode_e。通过函数api_key_evt_table_register(s_key_evt_table, KEY_EVT_MODE_MAX);对其进行注册。并在app层封装key_set_default_key_mode函数对所有按键的mode进行初始化,包括单耳模式、双耳模式。将函数key_refresh_key_mode_process放入main循环中,每当单双耳机模式切换时,便会对mode进行刷新,从而实现不同的模式下,按键配置不同的功能。

在app层demo中,key_refresh_key_mode_process刷新mode的函数还有一个变量为g_key_state.is_updata_mode,此值是为了实现处理单双耳机模式切换之外的场景来设置按键mode的情况,具体的流程可以自己设计。

(4) Demo简介

上电时,会先进行按键的初始化,配置引脚、事件触发时间及事件的回调等。

目前demo程序的流程如下:

(a) 按键动作扫描初始化

上电后,在user_init()函数中调用key的初始化函数key_init()或key_evb_init()。配置相应的控制io、按键按下的有效电平、各种事件的判定时间等,此部分可参考前几个小节的介绍,并调用API层函数api_key_add()注册添加,此时把相应的api_key_scan()函数添加到主循环中,便可以检测到按键的相应的动作了。demo程序将此函数添加到了app_key_task()中进行轮询执行。

与LED的初始化流程类似,在key_init()中,将相应的key_id添加到全局变量g_key_state.need_master_exec_id中进行记录,方便后续判断从耳对应的按键事件是否需要发送到主耳进行处理。

(b) 按键事件初始化

完成步骤1后,仅仅能检测到按键的动作,此时需要注册相应事件的回调处理。在demo程序中,提供了api_key_evt_happen_register()函数进行注册的接口,只需将回调函数的接口注册进来,同时将api_key_process()函数添加到主循环中即可。demo程序将此函数添加到了app_key_task()中进行轮询执行。

在按键初始化时,app层通过key_set_enable_flag()函数记录了按键的使能标志,方便后续对是否执行相应的按键处理进行判断。同时可通过key_get_enable_flag()获取相应的按键使能标志位。

(c) 按键mode事件注册

在demo程序的按键事件回调中,添加了mode事件的处理机制,其用意在于方便后续通过其它的控制方式更改按键的功能。具体介绍可参考上一小节的内容。

在初始化函数key_init()或key_evb_init()中,调用了函数api_key_evt_table_register()注册相关的事件表,其事件包括控制音乐的暂停播放、控制电话的接听挂断等,可通过其对应的枚举类型key_evt_mode_e去查询相应的事件,枚举类型的含义为注册的回调数组中相应的事件在此数组中的位置。注册成功后可通过api_key_evt_func_execute()函数执行相应的事件,app层将按键执行动作函数重新封装为tws_key_act()

demo中通过key_set_default_key_mode()函数设置相应按键对应的默认mode,可通过修改此函数的内容调整按键默认功能,此函数在初始化函数中上电即执行,如若通过其它方式(如保存到flash中)保留了上次运行时按键功能,可自行调整key_set_default_key_mode()函数调用位置。

app层提供了mode事件的刷新机制,将key_refresh_key_mode_process()添加到主循环中即可。demo程序将此函数添加到了app_key_task()中进行轮询执行。demo程序中,其刷新的方法有两种场景:

通过key_set_key_mode()设置相应的按键模式,可设置tws模式下或单耳模式下的mode。

当tws模式与单耳模式相互转换时,会在key_refresh_key_mode_process()函数中自动刷新为相应的mode。

(d) demo中happen事件处理函数简介

按键的初始化完成后,在按键执行时,如果是单耳状态会直接进行处理,如果是双耳状态,从耳事件需要发送给主耳进行处理。在执行按键的事件处理happen函数时,会对当前是否为从耳进行判断,以按键1的长按事件为例,在函数开始对is_need_snd_master变量进行赋值(下图标识1处),如果是从耳且需要发送给主耳时,此值为真,在标识2处会发送给主耳处理,如下图。

同时说明此函数标识3处,为特殊事件的处理,假如此按键在无来电时的功能为控制音乐播放下一曲,而在有来电时的功能为挂断电话,则可以将有电话的状态看作特殊状态,在此判断,并赋值特殊的mode进行处理。具体可参考代码逻辑。

happen事件处理函数

happen事件处理函数

(5) Demo按键默认功能

按键的默认功能配置可在key_set_default_key_mode中查询,分为左右耳、单双耳的功能。其中左右耳通过key_is_left()函数区分,g_key_event_tws记录双耳的功能,g_key_event_mono记录单耳的功能。使用通用开发板的话,左右耳是通过烧录时的USBID区分的,当USBID为0x20时,为左耳,否则为右耳。

另外,按键1的双击和长按有特殊的接打电话功能,默认双击接听、长按挂断或拒接。详见对应的happen回调函数key1_evt_dclick_happen和key1_evt_long_happen。

其默认功能如下表:mono-单耳模式,tws-双耳模式。

按键 模式 单击 双击 三击 长按(2~5s)
key1 mono左耳 音乐播放/暂停 语音助手
mono右耳 音乐播放/暂停 语音助手
tws左耳 音量- 下一曲 语音助手
tws右耳 音量+ 音乐播放/暂停 语音助手
key2 mono左耳 单耳保存用户区flash数据 单耳进配对模式 单耳进恢复出厂配置模式 单耳关机
mono右耳 单耳保存用户区flash数据 单耳进配对模式 单耳进恢复出厂配置模式 单耳关机
tws左耳 单耳保存用户区flash数据 单耳进配对模式 单耳进恢复出厂配置模式 单耳关机
tws右耳 单耳保存用户区flash数据 单耳进配对模式 单耳进恢复出厂配置模式 单耳关机
key3 mono左耳 音量+ 上一曲 语音助手 音乐播放/暂停
mono右耳 音量- 下一曲 语音助手 音乐播放/暂停
tws左耳 音量- 下一曲 语音助手 音乐播放/暂停
tws右耳 音量+ 上一曲 语音助手 音乐播放/暂停
key4 mono左耳 BT电话接听 BT电话挂断 BT电话拒接
mono右耳 LE电话接听挂断 LE电话接听挂断 LE电话拒接
tws左耳 BT电话接听 BT电话挂断 BT电话拒接
tws右耳 LE电话接听挂断 LE电话接听挂断 LE电话拒接

Flash用户数据模块

(1) 模块介绍

在API层,从flash的FLASH_SEC_0_ADDR与FLASH_SEC_1_ADDR两个起始地址开始,大小均为FLASH_SECTOR_SIZE字节的块定义为用户存储区。每个存储区分成大小为NVDS_STORE_UINT_SIZE的小节,定义为进行刷新存储的区域。每个存储区有NVDS_STORE_NUMBER个小节。根据不同的数据划分不同的tag_id(枚举类型nvds_tag_id_t),每个tag的数据最长长度为小节的长度NVDS_STORE_UINT_SIZE。可以通过添加tag_id保存新的数据到flash。其在存储时,每次保存都将会刷新flash的保存地址,按存储区的小节递增,如若FLASH_SEC_0_ADDR增加到了最大地址,则会擦除FLASH_SEC_1_ADDR的内容,并从第一小节处开始保存并累加,当FLASH_SEC_1_ADDR增加到最大地址时,又会擦除FLASH_SEC_0_ADDR的内容,以此类推。用户可根据修改相应app_config.h中相关的宏定义调整flash储存相关的信息等相关内容,具体可参考代码。

耳机上电后,会通过api_nvds_init函数初始化flash,并通过app_nvds_load读取用户区的数据到g_app_flash_user_data变量中,如上次关机时连接的手机的地址等,同时可修改app_nvds_update函数对需要保存到用户区的变量进行保存或删除。这些信息会在关机时自动保存到flash中。

如果使用的是通用开发板,在直接断电或者按下复位键时无法对现有信息进行保存,所以可以在复位或断电时使用debug指令"11 08"或关机指令对flash进行保存。

另外,此模块根据tag_id来保存数据到nvds数据区的方案,最终也是调用flash的读写函数,若用户直接调用flash读写,请注意flash的地址分配。

(2) Demo流程

(a) flash初始化

耳机上电后,在user_init()函数中调用flash的初始化函数api_nvds_init()对flash的区间进行初始化操作。

(b) 数据读取

flash初始化完成后,调用app_nvds_load()对存储在flash中的用户数据进行读取,并根据tag_id对相关数据进行判断与赋值。随后在执行过程中根据运行场景对相应的变量进行修改,目前出于安全考虑不会实时进行flash的读写,在关机时,会根据相关的变量对flash进行刷新。

(c) 数据更新

app层在关机或其它必要保存flash的场景下,通过调用app_nvds_update()函数,对各个tag_id的内容进行检测更新,除更新接口外,也封装了数据删除、保护等相关接口,可根据需要调用相关接口。

提示音逻辑

(1) 软件模块介绍

目前使用的提示音调用函数为tws_tone_play,其参数对应的值为e_type_tone_t枚举类。 有些提示音需要同步给双耳同步播放,如配对提示音。在tws_tone_play会对需要同步播放的提示音进行判断,并将其发送给从耳,使得两只耳机可以同时播放。如果不需要同时播放,则会直接调用tone_play()函数播放提示音。

在调试过程中,可以使用debug指令"11 0e +枚举值"来测试是否正确烧录提示音文件,如配对提示音"11 0e 00"。目前提示音的烧录地址为0xd0000。

(2) Demo流程

提示音的使用没有固定的初始化流程,只需要将制作好的提示音文件,正确烧录到相应的地址后,通过调用tws_tone_play()或tone_play()即可正常播放。

在demo程序中,统一使用tws_tone_play()具有判断是否需要主从耳同步播放的函数来播放提示音。如果在播放提示音的时候,又来了一个提示音,则会将新的提示音挂起,等待第一个提示音播放完成后,在tws_tone_delay_play_check_task()函数中再次调用tws_tone_play()进行新的提示音播放。如果新的提示音不需要同步,则会在tone_play()函数中将其挂起,在第一个提示音播放完成时,优先播放此提示音。

在需要播放提示音的场景里,BT连接提示音CONNECTED_TONE的调用比较特殊。其调用的位置在app_btble_connect_status_check()函数中,其原理是主耳在连接上BT后将ACL连接信息发送给从耳一同播放,涉及到几个变量:g_app_ctl.bt_conn_ready、g_app_ctl.bt_slave_ready、acl_link_tws.bt_ready以及当前连接状态的局部变量conn。

目前,g_app_ctl.bt_conn_ready与g_app_ctl.bt_slave_ready变量的变化如下:

g_app_ctl.bt_conn_ready:非从耳ACL连接成功场景下或主从耳各自断连时,变量置零;主耳发送信息时,将值赋值到acl_link_tws.bt_ready中发送给从耳;连接提示音播放完成时变量置一。

g_app_ctl.bt_slave_ready:非从耳ACL连接成功场景下或主从耳各自断连时,变量置零;发送ACL信息时,从耳回复后变量置一;播放连接提示音时,变量置二;连接提示音播放完成时,变量置三。

关机流程

(1) 模块介绍

在demo程序中,在一定时间内无连接,或电量过低时(通用开发板暂时没有此场景),会进入关机状态。其在app_poweroff_task函数中处理,所提及到的关机标志位有APP_SYS_FLAG_POWER_OFF_REQ和APP_SYS_FLAG_TIMEOUT_POWER_OFF_REQ两种。同时在关机时,会对用户数据进行保存。

可通过修改APP_ATUO_POWER_OFF_TO_MINUTES的值调整超时关机的时间。

如果使用的是通用开发板,可通过debug指令"11 0c",进入关机状态。

(2) Demo流程

(a) 目前关机的场景有以下几种

长时间低电量状态,导致电池电量不足时自动关机;

耳机入仓关盖后,自动关机;

没有BT连接、没有Dongle连接且不在配对模式,计时超过设定的时间会触发超时自动关机;

无连接状态下触发的配对,当配对超时失败时会自动关机。

(b) app_poweroff_task介绍

关机流程根据两个标志位分为两块,一个是通过使用app_sys_flag_set()函数将power off flag置位后直接进行关机的处理,此部分在关机前会进行关机提示音的播放以及BT的断连流程。最后调用app_system_poweroff()函数进行关机操作。

超时关机部分是在没链接且没在配对模式下的计时超时场景,当时间超过所设定的参数APP_ATUO_POWER_OFF _TO_MINUTES时,将会触发超时关机流程并由主耳调用函数接口app_tws_poweroff() (从耳触发时不会有相关动作),此函数用于主耳与从耳进行关机同步,后会同时调用app_system_poweroff()函数进行关机操作。

回连逻辑

(1) 模块介绍

在demo程序中,当BT超时断开或者开机时,读取flash有连接记录,app层通过将APP_SYS_FLAG_AUTO_RECON_REQ标志设置为真,执行回连动作,app层回连相关的程序在app_bt_recon_task函数中处理,其中,从耳不会进行回连动作,而是等待主耳连接。在此状态下,会设置回连的次数或者时间(一般设置时间),回连地址和需要连接的profile(只关心hfp、a2dp和avrcp),最后调用api_btrec_start函数将参数上传到api层进行处理,同时需要将api_btrec_process函数放在main循环中执行。

app层中,可通过g_app_ctl.app_recon_time_s对回连时间进行修改。

(2) Demo流程

在回连任务app_bt_recon_task()中,非从耳会根据本地连接记录去查询上次连接的BT设备的信息,此时查询的结果分三种情况处理:

(a) 有BT连接记录,不关心是否有dongle连接记录

此时会根据设备地址、回连涉及的profile,调用api_btrec_start()使能连接,连接处理流程在api_btrec_process()中执行。

(b) 只有Dongle连接记录

当只有dongle记录时,程序会将g_app_ctl.tick_delay_bt_paring赋初值,如果在两秒内dongle没有连接上,则会进入BT配对状态。

(c) 两者都没有连接记录

如果BT与Dongle都没有连接记录,则会马上进入BT配对状态。

APP层在调用api_btrec_start()的过程中,会对此时API层记录的回连超时时间与回连超时次数(ACL连接超时的次数,目前回连流程中每次对应的时间为RECON_ACL_TIMEOUT)进行判断,两者只需设置一个即可,在回连结束时,此值不会被清除,在下次进行回连时,如果没有设置新的超时时间或超时次数,会根据当时记录的参数进行回连。若在调用api_btrec_start()时,两者均为0,则会设置默认的超时时间API_BTREC_DEFAULT_TIME。如果两者都设置了非0值,则会根据先到达的参数结束流程,如超时时间设置为10s,超时次数设置为10次时,当回连超过10s但超时次数并未达到10次,依然会结束当前回连流程,并上报回连超时事件BTMISC_EVENTID_RECON_TIMEOUT到APP层。

api_btrec_set_con_time():设置超时的时间

api_btrec_set_con_counts():设置超时的次数

api_btrec_get_con_time():获取超时时间

api_btrec_get_con_counts():获取超时次数

api_btrec_get_con_remaining_time():获取回连流程剩余时间

api_btrec_get_con_remaining_counts():获取回连流程剩余次数

APP层可以直接调用相关函数进行超时时间与超时次数的判断。在demo程序中使用的参数为超时时间,在初始化时会进行第一次超时时间的设置。然后会通过全局变量g_app_ctl.app_recon_time_s进行判断并在回连任务中改变回连时间。使用此方案的目的是当从耳变为新主耳时,新主耳g_app_ctl.slave_recon_time_s所记录的时间继续进行回连流程,当本次回连流程结束后,下次回连可根据g_app_ctl.app_recon_time_s重新设置回连时间。此流程的细节转换比较多,可参考程序。

API层回连是围绕执行处理函数api_btrec_process()所设计的。

api_btrec_process()函数

程序中分为超时和超次两块判断,调用api_btrec_set_state改变回连状态,此函数一般不需要APP层调用。在APP层关注api_btrec_start()和api_btrec_stop()两个函数即可开始回连和停止回连。

api_btrec

当API层回连状态改变时,会在api_btrec_process()中调用到相应的处理函数,此处的函数大多有上报相关事件到APP层,事件枚举类型为btmisc_eventid_e,在app层使用注册接口api_btmisc_event_user_register()注册后即可完成上报处理。

BTMISC_EVENTID_RECON_SUCCESS:回连成功事件

BTMISC_EVENTID_RECON_CANCEL_BYLOCAL:本地取消回连事件

BTMISC_EVENTID_RECON_WAIT_MASTER:等待主耳并取消回连

BTMISC_EVENTID_RECON_TIMEOUT:回连超时事件

BTMISC_EVENTID_RECON_COUNTS_MAX:回连超次事件

BTMISC_EVENTID_RECON_RETRY:重新尝试ACL连接

BTMISC_EVENTID_RECON_WAITECONN_PROFILE:等待profile连接

BTMISC_EVENTID_RECON_NEW_DEVICE:连接新设备事件

BTMISC_EVENTID_RECON_POSITIVE_FALSE:回连时positive为false

BTMISC_EVENTID_RECON_PAGE_SUCCESS:回连时page成功

demo程序中目前只关心超时和超次事件,其他事件并未注册。

api_btrec_xxx_func()函数

回连工程中类似api_btrec_xxx_func()函数,用于在回连过程中处理相关流程所封装的函数,例如在ACL连接完成时,host上报的ACL连接完成事件会对api_btrec_aclconn_complete_func()函数进行处理,此类函数是必要的,否则回连流程可能出错。

BT配对与恢复出厂设置

(1) 模块介绍

在demo程序中,如果使用的是通用开发板,双耳可同时双击key2按键或发送debug指令"11 0b 00"进入配对模式,如果使用的环境为带仓的耳机套件,两只耳机入仓后长按仓按键3s即可进入配对模式。其配对流程标志为APP_SYS_FLAG_PAIRING_MODE_REQ。

同样,如果使用的是通用开发板,双耳可同时双击debug指令"11 0b 01"进入双耳组队模式(恢复出厂设置),如果使用的环境为带仓的耳机套件,两只耳机入仓后长按仓按键10s即可进入组队模式。初次开机或全擦flash后,双耳没有组队时,可通过此指令进行双耳组队。双耳组队模式(恢复出厂设置)标志为APP_SYS_FLAG_FACTORY_RESET。

这两部分代码都在app_bt_pairing_task中,即:

APP_SYS_FLAG_PAIRING_MODE_REQ值为真,APP_SYS_FLAG_FACTORY_RESET值为假时,为配对模式流程。

APP_SYS_FLAG_PAIRING_MODE_REQ值为真,APP_SYS_FLAG_FACTORY_RESET值也为真时,为双耳组队流程(恢复出厂设置)。

注意:

双耳组队模式会清除用户区数据。

(2) Demo流程

当APP_SYS_FLAG_PAIRING_MODE_REQ被置位时,会进入app_bt_pairing_task()函数进行处理。首先进行清除当前BT状态的动作,当BT断连成功后,如果仅有配对模式的标志被置位,则会调用app_bt_enter_pairing()开启scan,并播放配对提示音。

如果APP_SYS_FLAG_FACTORY_RESET标志同时被置位,则会根据双耳组队的模式TWS_PAIRING_MODE_WIRELESS (无线组队或有线组队)执行相应的组队流程。同时清除用户区nvds中保存的用户数据、清除保存的连接信息、恢复默认的按键操作等流程,后续可以在这里添加需要重新设为默认的配置信息。

BT scan管理

(1) 模块介绍

在demo程序中,app层scan管理部分在app_bt_scan_task中,可通过函数app_bt_write_scan_enable(uint8_t en, uint8_t call_num)对scan进行使能或不使能。此函数第一个参数en可参考bb_scan_en_t类型,第二个参数call_num为调试打印log,用于判断函数调用的位置。

app层中,可通过g_app_ctl.app_inqscan_time_s、g_app_ctl.app_pagescan_time_s对scan时间进行修改。

(2) Demo流程

app_bt_scan_task()函数

app_bt_write_scan_enable()函数调用了api层api_btscan_enable()函数进行状态的使能。scan的处理与回连的处理流程方案类似,将api_btscan_process()添加到scan任务app_bt_scan_task()中执行。此函数上半部分为scan的超时时间设置,使用全局变量g_app_ctl.app_pagescan_time_s、g_app_ctl.app_inqscan_time_s进行记录,在初始化时调用API层函数进行第一次赋值,后续当全局变量改变时,会在app_bt_scan_task()进行刷新。相关的API层设置时间的函数如下:

api_btscan_set_inq_time():设置inquiry scan超时时间

api_btscan_get_inq_time():获取inquiry scan超时时间

api_btscan_refresh_inq_start_tick():重新计时inquiry scan

api_btscan_get_inq_remaining_time():获取inquiry scan剩余时间

api_btscan_set_page_time():设置page scan超时时间

api_btscan_get_page_time():获取page scan超时时间

api_btscan_refresh_page_start_tick():重新计时page scan

api_btscan_get_page_remaining_time():获取page scan剩余时间

函数app_bt_scan_task()的后半部分是对scan失败时的逻辑处理。具体流程可参考代码。

API层scan管理是围绕执行处理函数api_btscan_process()所设计的。

api_btscan_process()函数

上图是对scan的计时操作和超时的判断,调用api_btscan_set_xxx_state改变scan状态,此函数一般不需要APP层调用。在APP层关注api_btscan_enable()函数的使用即可。

api_btscan

当API层scan状态改变时,会在api_btscan_process()中调用到相应的处理函数,此处的函数有上报相关事件到APP层,事件枚举类型为btmisc_eventid_e,在app层使用注册接口api_btmisc_event_user_register()注册后即可完成上报处理。

BTMISC_EVENTID_SCAN_INQ_CANCEL_BYLOCAL:本地取消inq_scan流程

BTMISC_EVENTID_SCAN_PAGE_CANCEL_BYLOCAL:本地取消page_scan流程

BTMISC_EVENTID_SCAN_INQ_TIMEOUT:inq_scan超时流程

BTMISC_EVENTID_SCAN_PAGE_TIMEOUT:page_scan超时流程

demo程序中目前只关心两个超时事件,其他事件并未注册。

与回连类似,在ACL连接完成时,host上报的ACL连接完成事件会对api_btscan_aclconn_complete_func()函数进行处理,此函数是必要的,否则scan流程可能出错。

TWS耳机组队及配对模式

TWS地址说明,SDK工程里有三个地址,分别是:

  • g_app_flash_user_data.const_addr:下载时写入的地址,不会改变(FLASH 0x1DF000位置 )。
  • g_app_flash_user_data.new_alloc_addr:组队后使用的地址,会设置为TWS工作的地址。
  • g_app_flash_user_data.cur_used_addr:耳机开机生效实际使用的地址(const_addr或者是new_alloc_addr)。

耳机在两个场景下需要进行组队:

  • TWS耳机出厂时候两只单耳要组合成对耳形式工作。
  • TWS已经组队的耳机其中一只被替换掉了。

组队目的:

  • 组队的目的是确定双耳使用的蓝牙地址是左耳的地址,及双耳的new_alloc_addr都会被赋值为左耳的const_addr。
  • 统一双耳的async channel id和access code,以便之后双耳随时能同步作为TWS模式的主副耳工作。

组队动作的触发:

在通用开发板上三击k2按钮,在B91 TWS耳机套件长按绿色按键10s后都会触发APP_SYS_FLAG_PAIRING_MODE_REQ和APP_SYS_FLAG_FACTORY_RESET两个事件,组队时擦除BT配对历史记录,擦除双耳new_alloc_addr、双耳async channel id和access code、设置的按键模式等用户信息,以及EQ参数等也会恢复出厂设置(如果两只耳机判断上次组队对方地址有变化也会擦除dongle的配对信息)。

(1) 有线组队

在有仓的应用场景下,有线组队方式通过仓交换双耳地址,然后双耳各自保存对方地址,并把左耳地址赋值给本地的new_alloc_addr,同时根据左右耳地址计算出双耳之间工作的async channel ID、access code。如果本地之前保存的对方地址与本次收到的不同,则认为是一只耳机被替换掉的场景,各自会擦除dongle的配对信息,更新新的async channel ID、access code。详见\src\vendor\_proj_cc_tws_\batt_uart.c文件里的uart_chn_ac_addr_gen()函数实现。

而后左右耳用固定的async channel ID、access code进行连接确定主副耳(期间会过滤掉非对方地址的包,避免多对耳机同时配对组队错乱)。

有线组队流程图

(2) 无线组队

在没有仓的应用场景下,比如soundbar、demo的通用开发板,SDK提供无线的方式实现双耳组队的功能(设置宏TWS_PAIRING_MODE_WIRELESS为1使能)。区别是双耳在用固定的async channel ID、access code进行连接确定主副耳后,进行双方地址的交互,双耳用左耳的地址赋值给各自的new_alloc_addr,并以此计算出双耳工作的async channel ID、access code即完成组队。无线组队有个缺点就是如果多对耳机近距离同时组队时,耳机接收的空中交互包可能不是期望组队的耳机发出的导致交叉错误。

(3) 耳机配对新设备

TWS耳机进入配对模式后,会开启BT的page scan和inquiry scan,可以被手机搜索配对连接,或者被配对模式下的dongle配对连接上(dongle也在配对模式)。但是每次进配对模式后配对BT和dongle是二选一,配对上一路后,另外一路会退出配对模式。

耳机或者dongle进配对模式后,耳机和dongle的async channel ID、 access code都会设置为固定的相同参数以便可以通信上,双耳会先sync上,确定主副耳关系,然后在与dongle的配对交互过程中,主耳会把左右耳之间已经确定的私有async channel ID、 access code告诉dongle以便后面建立连接,配对结束后耳机和dongle会退出固定的async channel ID、access code,进入私有的async channel ID、 access code建立连接,所有dongle配对上耳机后会有掉线再回连的过程。

(4) 耳机回连dongle

耳机和dongle在配对后已经知道了彼此的async access code,只要耳机不在配对模式下,都可以被上次连接过的dongle回连上。

TWS双耳主从切换

TWS耳机在主耳关机、双耳同时在仓内从耳出仓、双耳在仓外主耳入仓的场景下会触发主从耳机模式切换,期间主耳会同步为从耳继续工作。目的是在主耳关机情况下原从耳继续保持跟手机或者dongle的连接,或者确保仓外的耳机的MIC是有效的(只有主耳MIC有效)。主从切换动作通过消息TWS_FLAG_HANDOVER_START触发。

在主从切换的最后阶段主耳调用app_bt_handover_cfm()函数将主耳相关的状态信息同步给从耳,从耳调用btp_tws_handover()赋值给对应的变量。内容主要包含各个profile的状态,比如当前通话状态、rfcomm层收发数据credits数据、最后的L2CAP层CID等。

TWS双耳通信接口

TWS主从耳之间链路虚拟、状态同步、UI交互都需要数据通道。SDK提供两个接口供调用,app_push_cmd()、async_tws_push_cmd()。前者发送buffer长度不能超过37字节,后者长度没有限制,函数内部会做拆包处理。接收方会在函数tws_cmd_vendor_parse()里对数据进行解析处理。

TWS模式从耳BT链路的虚拟

TWS双耳连接上后,手机搜索配对连接都是跟主耳的交互,对手机而言只有一只耳机存在。从耳必须知道主耳跟手机之间的配对加密信息及链路信息,拟出一样的链路才能正常接收到手机发送给主耳的数据包。

主耳与手机建立连接后,会调用bt_ll_tws_flag_set();设置本地的连接flag, 比如ACL链路建立后调用bt_ll_tws_flag_set(TWS_FLAG_ACL_CONNECTED, 1); 置位TWS_FLAG_ACL_CONNECTED flag, 链路加密结束后置位TWS_FLAG_ACL_ENCRYPTED, 其他Profile flag定义为:

#define TWS_FLAG_A2DP_CONNECTED             (0x01UL << 1)
#define TWS_FLAG_AVRCP_CONNECTED            (0x01UL << 2)
#define TWS_FLAG_HFP_CONNECTED              (0x01UL << 3)
#define TWS_FLAG_SPPATT_CONNECTED           (0x01UL << 4)

另外主耳本地用变量tws_sync_link_flag.slave_link_flag来记录当前从耳的链路flag状态。主耳在void app_system_sync_link_task(void)任务函数里检查当前本来链路跟从耳链路信息的差异,主耳在ACL链路建立并加密成功后,会按照ACL、A2DP、AVRCP、HFP、GATT/SPP的顺序把对应的链路信息同步给从耳。从耳在ACL链路虚拟后,就可以收到手机发送给主耳的数据包,期间如果profile没虚拟出来对应的包在HOST层会丢掉不处理。

主耳调用tws_link_info_send()函数把对应的链路信息发送给从耳,从耳在函数app_tws_tsync_cmd_rcvd_callback()解析处理并调用对应接口虚拟链路,见函数\src\stack\bt\bt_tws\btp_tws_slave.c。结束后,发送TWS_CMD_SLAVE_SYNC_LINK_RSP消息给主耳告诉主耳当前从耳的链路信息(主耳以此更新tws_sync_link_flag.slave_link_flag)。

Dongle端UI及APP层说明

Dongle端UI说明

Dongle端key及LED的添加注册同耳机端,见章节LED功能Key功能,此处不再累述。

SDK默认配置通用开发板只有KEY1按键的双击有效,对应动作是擦除跟耳机的配对信息,同时进入配对模式。LED只定义对应GPIO_PB7 IO的红色LED有效,灯效效默认配置见耳机和dongle在各个模式下的灯效章节说明。

Dongle端按键只触发进配对,不参与电话、音乐相关的行为控制。Dongle电话及媒体控制均由耳机端触发,并通过ASYNC_CMD_UI_MEDIA_KEY消息将键值发给dongle,dongle端由app_ui.c里的函数tlk_app_cmd_rcvd()解析并转换为hid命令通过USB发送给连接dongle的主设备执行动作。

Dongle端用户区保存:

Dongle端用户去数据结构为:

typedef struct _bt_ram_para { 
unsigned short record_crc; 
//above field will write in flash 
uint32_t ble_ac; 
uint8_t ble_ch; 
uint8_t ble_id; 
bd_addr_t addr_headset; 
bd_addr_t const_addr;//be writeed when down code 
uint8_t const_name[LOCAL_NAME_LEN_MAX];//be writeed when down code 
}__attribute__((packed)) bt_ram_para_t; 

Dongle与耳机配对后,stack会发送ASYNC_EVENT_PAIRING_SAVE消息触发实时写入flash,位置在flash 0xE0000开始的4k位置。

  • ble_ac dongle:双击key1,配对耳机后耳机发给dongle的access code。
  • ble_ch dongle:双击key1,配对耳机后耳机发给dongle的channel ID。
  • ble_id dongle:双击key1,配对耳机后耳机发给dongle的编码序列号(目前SDK只支持配对一个dongle,都会是0)。
  • addr_headset:双击key1,配对耳机后耳机发给dongle的TWS使用的地址,即耳机端保存的g_app_flash_user_data.new_alloc_addr值。
  • const_addr及const_name:赋给dongle的地址和名字,目前SDK没有使用。

Dongle用户数据在双击key1时会擦除并实时写回flash,所以dongle在配对模式下掉电再上电,dongle默认会进入配对模式。

耳机及dongle固件升级

OTA升级

SDK支持OTA无线升级功能,IOS设备通过BT GATT profile、android设备通过BT SPP profile传输OTA数据包。新的固件数据暂时存放在芯片内部flash固定区域(耳机是0x120000--0x1d7000, dongle是0x70000--0xe0000),升级结束后在BootLoader阶段判断并拷贝到0x9000开始的执行区域,然后重启生效。

耳机端flash分布图:

耳机端flash分布图

OTA流程如下(以升级TWS耳机为例):

OTA流程图

Dongle端的OTA流程一样,只不过所有的数据包都通过主耳作为中继转发。APP发送OPC_REQ_VERSION获取当前固件版本号,判断如果新版本号大于目前的版本号则发送OPC_REQ_UPDATE请求升级,耳机根据数据包里的版本号编码规范判断升级耳机还是升级dongle(耳机是11.XX,dongle是01.XX),在函数firmware_update_req_rsp()处理记录当前OTA的文件大小、CRC等内容,并回送OPC_RSP_UPDATE给APP端。

而后开始升级,每次调用firmware_page_request()发送OPC_REQ_DATA向APP要256字节的固件信息,request内容包括当前256字节数据相对于0x9000开始位置的偏移地址,如果是断点继续OTA则这个偏移地址为上次失败前的位置(保存在flash用户区里),然后APP通过OPC_RSP_DATA返回要求的256字节内容。如此循环直到文件读取结束。

OTA升级时APP是从目标bin文件0x9000位置读取,前面的boot相关代码不参与升级。

关键函数:

  • dfu_rx_callback():耳机接收来自APP或者dongle的升级相关数据的处理,dongle接收来自耳机的数据处理。
  • rsp_data_relay():升级dongle时主耳调用该函数拆包转发数据给dongle端。
  • flash_page_save():耳机和dongle调用该函数写flash。
  • firmware_page_request():耳机或者dongle调用该函数发送OPC_REQ_DATA给APP要下个固件数据包。
  • update_app():耳机和dongle OTA结束且校验成功后调用该函数将新的固件有效信息写入到flash DFU_NEW_FIRMWARE_INFO_ADDR (0x8800)位置,待重启后检查并搬运到执行区域生效。

Boot模式下的有线升级

在B91 TWS耳机套件环境下支持通过仓板进行有线方式的单线升级(仅限TWS耳机端),目的是产线先行生产,迭代软件版本后快速升级版本。激活方式是在用户模式下,双耳开机处于入仓状态,短按B91 TWS耳机套件的耳机基板上的红色按键触发,此时仓会分别给左右耳发送{0xca, 0xfe, 0xa3, 0x85, 0x00}命令序列,耳机在函数batt_box_cmd_handler()的开始解析处理该命令,并调用tlk_afh_boot_mode_set()向寄存器WORK_MODE_AREG写入0xc5, 然后重启系统。系统重启后判断该寄存器数据为0xc5则进入单线升级模式(serial_boot_task())。见src\vendor\_proj_boot_device_\app.c里函数user_init_normal()的处理。

在单线升级之前,需要提前把新的耳机bin文件下载到B91 TWS耳机套件内置芯片flash的0x80000的位置,提示音下载到B91 TWS耳机套件内置芯片flash的0x150000位置,0地址开始为仓自己的固件。见下图:

bin文件下载设置

单线升级开始后,会从B91 TWS耳机套件内置芯片flash的0x88000(耳机固件的前36k的boot代码不参与升级)位置开始,镜像copy到耳机端flash 0x8000的开始位置,copy长度为0x110000(1088k),由于耳机tone音需要存放在耳机flash的0xd0000位置,所以tone音需要提前下载到仓的flash的0x150000位置。Copy结束后仓会发送CMD_CHECK_FLASH(0x91)命令触发耳机校验crc并将结果发回给仓,仓判断正确后发送CMD_REBOOT(0x86)触发耳机重启,重启会根据0x8000位置信息校验0x9000开始位置的bin的完整性。

与OTA升级不同的是,单线升级直接对耳机执行区域进行擦除和写操作。

Boot模式介绍

Boot的bin通过编译_proj_boot_device_项目生成,路径为src\_proj_boot_device_\output\_proj_boot_device_.bin,使用时要copy到src根目录下,耳机和dongle共用。耳机编译后生成_proj_cc_tws_.bin,然后编译环境会运行bin_ota.exe将_proj_boot_device_.bin和_proj_cc_tws_.bin打包为_img_proj_cc_tws_.bin,_proj_boot_device_.bin会放置于0地址开始的位置(0地址--0x7fff的36k范围),_proj_cc_tws_.bin放置于0x9000开始的位置。并将耳机版本号(存放于src\ver.ini里)、耳机实际bin开始位置、bin size、bin crc和编译日期写入到0x8000开始的位置。如下图_img_proj_cc_tws_.bin对应位置的截图:

_img_proj_cc_tws_.bin对应位置

Boot模式下除了单线升级功能(见章节Boot模式下的有线升级)外还有下面两个功能:

(1) 读取flash 0x8800内容判断是否有有效的OTA bin待生效,如果有,则将0x120000(dongleshi 0x70000)位置开始临时存放的新的bin copy到0x9000开始的位置,校验成功后,擦除flash 0x8000开始位置之前的bin信息,包括ota image信息,把新的bin相关信息(size、crc、开始地址)写入到0x8000的位置,并重启,见src\vendor\_proj_boot_device_\app.c里函数new_firmware_apply()的实现;

(2) 如果没有新的OTA bin待生效,检查0x9000开始的bin的完整性,如果正常,则跳转到执行0x9000开始的位置并启动。见src\vendor\_proj_boot_device_\app.c里函数check_image_integrity()的实现。

Boot模式下没有USB打印log的功能,可以通过定义宏UART_DEBUG_SWS_MODE为1使能通过SWS线模拟UART打印。具体实现见src\vendor\_proj_boot_device_\printf.c。

音频通路

耳机音频通路相关

Audio path结构框图

Audio path包含的模块:BT audio, Low latency audio, tone, codec interface, audio control。框图如下所示:

BT audio负责处理BT音乐和BT电话相关的逻辑, 包含的功能:接收A2DP数据,收发SCO数据, 处理MIC上行数据,音频数据的编解码,BT audio同步机制,包括主副耳之间的同步以及耳机和手机的同步。

Low latency audio负责处理2.4GHz低延时私有协议模式下的music和voice相关的逻辑,包含的功能:接收music数据,收发voice数据, 处理MIC上行数据,音频数据的编解码,主副耳之间音频同步机制。

Codec interface包含codec相关的处理函数,例如把PCM数据放入codec playback buffer。

Audio control包含了audio path模块和BT应用以及BT stack交互的接口、audio path初始化逻辑以及audio模式切换的逻辑。audio有两种模式AUDIO_MODE_LE和AUDIO_MODE_BT, AUDIO_MODE_LE表示当前是2.4GHz低延时私有协议下的音频,AUDIO_MODE_BT表示BT audio。

Audio path结构框图

BT voice和low latency audio混音

BT voice和low latency audio混音默认开启,可以通过宏LE_AUDIO_BT_VOICE_MIX_ENABLE来关闭混音功能。

混音功能默认开启

混音模式下low latency audio的音量被设置成150,范围是0~1024,混音模式下low latency audio听起来就像是背景声,音量较轻,可以通过ll_audio_set_mix_vol修改混音模式的音量。

修改混音模式的音量

音乐一拖二模式

BT music和low latency audio也支持混音功能,但由于B91平台上RAM资源的限制,所以默认是关闭的。

混音功能默认关闭

音乐一拖二模式下,在不开启混音功能时默认BT music优先级高于low latency audio,BT music会抢占low latency audio播放出声。可通过宏APP_DONGLE_MUISC_PRIORITY_HIGHT修改一拖二优先级,默认值是0表示BT music优先级高于low latency audio。

设置优先级

提示音制作

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

工具:wave_to_tone_bin_tool_v1.0.1

制作提示音步骤:

根据tone.h定义的type, 把音源改成1,2,3,4,5...命名, 如下是type和音源的对应关系。修改好音源名字后用pcm16to8_vol_1024.bat脚本生产tone文件,生产的文件是ADPCM格式。

提示音制作步骤

提示音播放步骤:

调用tone_play API指定id, 例如tone_play(CONNECTED_TONE),程序会初始化ADPCM。

在BT audio播放逻辑或low latency audio播放逻辑中调用tone_get_sample获取PCM数据。

Dongle端音频通路相关

Dongle音频通路工作方式

Dongle在插入USB主机后会枚举成一个USB MIC设备和一个USB扬声器设备。dongle正常工作时,将来自耳机的音频处理后通过USB MIC通路上传给USB主机,将来自USB扬声器通路的音频数据处理后发送给耳机。

Dongle音频通路流程

dongle音频通路流程

(1) Dongle接收到来自耳机的单通道16kHz采样率mSBC编码的MIC数据包,对应的函数接口为app_audio_receive_voice_data();

(2) 在MIC任务中主要对MIC数据进行解码、数据对齐、采样率转换和音量调整,对应的函数为app_mic_task();

(3) USB iso in处理中将②中处理后的数据发送给USB主机,对应的函数为app_usb_iso_in_handler()。

(4) USB iso out处理中接收到来自USB主机的48kHz采样率的PCM格式的speaker数据,对应的函数为app_usb_iso_out_handler();

(5) 在audio任务中主要对speaker数据进行音量调整、数据对齐、编码成LC3格式,对应的函数为app_audio_task();

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

音频算法处理

音频算法概览

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

  • EQ ( Equalizer ):均衡器
  • SBC ( Sub-band Coding ):子带编码
  • mSBC ( Modified Sub-band coding ):改进的子带编码
  • CVSD ( Continuous Variable Delta Modulation ):连续变量增量调制
  • AAC ( Advanced Audio Coding ):高级音频编码
  • LC3 ( Low Complexity Communication Codec ):低复杂度通信编码
  • BF ( Beamforming ):波束成形
  • AEC ( Acoustic Echo Canceling ):回声消除
  • NS ( Noise Suppression ):降噪
  • AGC ( Automatic Gain Control ):自动增益控制
  • PLC (Packet Lost Compensation):丢包补偿

算法运行框图

根据音频通路的不同运行状态,将音频算法区分为如下图所示的三种模式:

Voice uplink为语音上行通路,其中BT支持mSBC和CVSD两种编码算法,LL只支持mSBC一种。语音算法包括BF->AEC->S_NS和BF->W_NS->AGC两条通路,受限于硬件资源,该两条通路二选一生效。

Voice downlink为语音下行通路,其中BT支持mSBC和CVSD两种解码算法,LL仅支持LC3解码算法。

Music downlink为音乐下行通路,其中BT支持AAC和SBC两种解码算法,LL仅支持LC3解码算法。

算法运行框图

EQ

方案概述

EQ执行逻辑如下图所示,EQ部分支持在线更改参数,实现多音效切换的功能,开机时需要先从指定的flash地址中将最新参数加载进来,也可不使用该功能(flash地址传空),默认使用定义在tlkalg_eq_interface_api.h中的默认参数。参数加载时内部自行计算算法所需系数,并存储在内部全局数据中,供数据处理时使用。

参数加载完成后,根据使用需求,如协议栈与手机端协商结果,对采样率进行更改,采样率更改接口内部自行判断是否需要进行更改,默认使用48kHz,若需求采样率与默认采样率不同,内部进行采样率更改并计算算法系数。

根据当前使用环境将算法系数计算完毕后,在音频数据流适当位置调用数据处理接口即可。

系统关机时将当前最新的EQ参数保存到指定的flash地址,系统下次开机时恢复该音效,flash地址为空时该功能不生效。

音乐模式下支持9阶滤波器,默认使用9阶,通话上行和下行支持4阶滤波器,默认使用2阶。

EQ为可选功能,通过下述宏可控制整体EQ功能及语音模式下EQ功能使用与否:

#define EQ_ENABLE                   1
#define EQ_VOICE_ENABLE             1

EQ执行流程图

参数格式

默认配置参数使用9阶滤波器,具体参数对应上述滤波器类型和主要参数,从左至右依次为增益(2B)、中心频率(2B)、Q值(1B)、滤波器类型(1B)、预留位(1B)。表头数据依次为索引(1B)、最大增益数量(1B)、阶数(1B)、总增益(1B),其中索引用于区分不同的EQ类型,总增益保证所有频点的增益值均位于0dB以下。

参数格式

配置工具

同步提供下图所示EQ参数图形化配置工具,区域1可对每阶滤波器参数进行具体配置,包括滤波器类型、Q值、中心频率和增益值,区域2用于设置整体参数,包括采样率、通道类型、滤波器阶数、EQ模式和总增益,其中总增益是由工具内部算法根据各滤波器参数自动生成的,不支持手动更改,区域3用于显示更改区域1滤波器参数的实时频响曲线。

区域5处勾选“All Para”后点击“Get Parameter”,可在区域4处看到如下图所示的EQ参数,该参数可直接放置于代码中。区域5同步支持参数保存,可将当前设置的参数保存下来,便于后续查看。该工具同时支持在线调试功能,分为有线USB和无线RF两种方式,为实现该功能,设备端需要同步支持相应的通信方式,点击“Download”可将区域4显示的参数加载至设备端。

EQ参数图形化配置工具

SBC

算法执行流程如下图所示,按数据传输方向及数据类型可分为三类,分别为SBC解码应用于音乐下行、mSBC解码应用于语音下行、mSBC编码应用于语音上行。

BT/低延时双模在线通用SDK中音乐应用时,设备端仅作sink,因此SBC仅需支持解码。音乐解码时SBC与AAC互斥存在,且AAC所需内存空间大于SBC,为节省算法占用空间,SBC解码时共用AAC的buffer size。初始化时将分配的内存起始地址及SBC相关参数(采样率、比特池深度、通道数等)传递给初始化接口即可,根据所要解码的数据类型(左声道、右声道、立体声)调用对应的解码函数完成解码操作。

mSBC的编解码与重采样算法共用一个buffer,在初始化前先获取占用空间,便于为后续算法进行地址分配。编解码的初始化操作与SBC相似,给定起始地址及参数即可,编码算法在初始化之后需要设置一下块数和比特池深度。在语音数据传输的相应位置调用编解码处理函数,即可完成对应处理,编码时额外封装了一个带参数指针的编码接口,用于双声道编码时对另一声道数据进行编码。

SBC算法执行流程

mSBC丢包补偿

mSBC丢包补偿算法用于在语音下行通路检测丢包状态,若发生丢包则在时域进行补包处理。

算法运行流程如下图所示,通过get_size函数获取到算法运行所需内存空间大小,在指定buffer上分配出对应空间,mSBC_PLC与CVSD_PLC互斥生效,因此在分配空间时选择两者中较大者进行分配,可节省部分内存。在进行codec voice模式初始化时同步初始化该算法,将前述分配的内存起始地址、采样率及帧长传输给算法初始化接口即可。在语音下行处理通路中进行丢包检测,根据丢包情况分别调用相应的算法处理接口。

mSBC算法执行流程

CVSD

CVSD用于BT语音通路中的数据编解码及语音下行时的丢包补偿。

CVSD及CVSD_PLC算法执行流程如下图所示,语音通路中SDK默认使用mSBC编解码,当接收到的包信息中指明当前使用CVSD编解码格式时才进行CVSD初始化。

CVSD_PLC通过get_size获取所需占用内存空间,且与mSBC互斥生效,以两者中空间占用较大者为准,可节省部分RAM资源占用。语音数据上行编码时直接调用编码接口即可,下行解码时根据是否发生丢包,进行解码或补包操作。

CVSD算法执行流程

AAC

AAC在SDK中应用于BT通路下音乐数据的解码。

算法运行流程如下图所示,AAC运行需要两部分内存空间,一部分为该算法独占的空间(pBuf),一部分为可与其他算法共享的空间(pScratchBuf),在设置空间起始地址时同步考虑该两部分空间。AAC与SBC不会同时生效,pBuf可与SBC共用同一块内存空间,赋相同的空间起始地址。AAC所用空间大于SBC,两个算法均按AAC空间大小分配内存。初始化时将分配的两部分内存起始地址及配置参数传递给接口,参数已在算法文件中以全局变量的形式定义好,外部不用再进行设置。在音频数据下行通路中,根据数据类型调用相应解码接口,BT/低延时双模在线通用SDK为TWS耳机方案的应用,默认使用left解码接口。系统默认使用SBC解码,在A2DP数据接收过程中,识别到传来的数据为AAC编码,才会进行AAC算法初始化及解码函数注册。

AAC算法执行流程

LC3

LC3应用于dongle和耳机间音乐数据传输的编解码,编码流程运行在dongle上,解码流程运行在耳机端,语音下行数据的解码也使用该算法。

算法运行流程如下图所示,获取内存占用大小接口封装为综合接口,传入内存类型即可返回对应的内存空间大小,内存类型参看枚举e_lc3_buff_type_e。BT/低延时双模在线通用SDK中设计了LE音频与BT音频混音的功能,因此LC3解码所需的两组内存不与其他算法共享,否则在混音开启时会导致异常,定义了两个全局数组g_lc3_dec_buff、g_ll_lc3_dec_scratch_buff供LC3解码使用,dongle端的编码同样也定义两组全局数组g_lc3_enc_buff、g_lc3_enc_scratch_buff。初始化时需要传入两组内存起始地址,及配置参数(比特率、通道数、采样率等),配置参数以全局变量的形式定义在算法文件内部,外部使用时不需设置相关参数。上述流程执行完成后,在数据传输通路中调用相应的编解码接口即可。

LC3算法执行流程

EC

本算法是几个算法的集合,通常包括BF、AEC、NS、AGC。通过宏配置选择运行合适的算法。本SDK使用的语音上行通路算法为:BF,NS,AGC的组合。

在SDK语音上行通路中,使用双MIC(麦克风)方案,语音MIC和噪声MIC采样两路数据,送入BF算法,经过BF算法对特定角度的语音信号增强,对其他角度的噪声信号进行衰减处理。经过BF处理的数据已经变为单声道数据,再送入NS模块,该模块进一步对语音信号中的噪声处理,并对信号进入该算法的增益和输出增益进行调整。经过NS处理后进入AGC模块,该模块对信号不同电平增益进行不同的调整,保证信号不会饱和。经过AGC后进入EQ,对信号进行整形润色。

本SDK因系统资源受限,主要有两种选择配置通路,一种是BF、W_NS、AGC通路,一种是BF、AEC、S_NS通路。

BF

BF算法基本调用流程如下:

(a)初始化BF算法。先是通过gsc_get_version()得到算法的版本号,再通过gsc_get_size()函数得到算法所需空间,从系统分配空间给BF算法,通过gsc_state_init()进行BF算法初始化。

(b)每得到一个frame的双MIC数据送入gsc_BeamFormer()算法执行处理,得到一个单声道音频信号。

BF算法调用流程

在SDK中可以通过宏使能BF算法生效,该宏的开和关可以控制BF算法中所有BF的相关配置。该算法的宏总开关为0时,表示关闭BF算法,为1时表示打开该算法。

#define ENC_GSC_ENABLE                                  0

因BF算法与硬件设计及音腔模具有强相关性,所以不同项目需要对BF算法进行优化调整BF参数,在SDK中可以通过BT SPP对BF算法抓取数据进行分析更新参数,进一步优化BF性能。

AEC

AEC算法应用于从麦克风的输入信号中,消除喇叭和麦克风之间的路径而产生的回音。目前算法通过自适应滤波器以及一个非线性处理模块,对回音进行消除。

算法基本调用流程如下:

(a)初始化AEC算法。先是通过tlka_aec_ns_get_version()得到算法的版本号,再通过tlka_aec_get_size()函数得到算法所需空间,从系统分配空间给AEC算法,通过tlka_aec_init()进行AEC算法初始化。

(b)每得到一个frame的单声道MIC数据或双MIC处理后的数据,与单声道speaker数据一起送入tlka_aec_process_frame()算法执行处理,得到一个去除回声信号的单声道音频信号。

AEC算法调用流程

在SDK中可以通过宏使能AEC算法生效,该宏开关控制AEC算法所有AEC的相关配置。该算法的宏总开关为0时,表示关闭AEC算法,为1时表示打开该算法。

#define AEC_ENABLE                                  0

NS

在本SDK中支持两种类型的NS,一种基本性能的S_NS算法,一种是W_NS,在本SDK中默认使用的是W_NS,本节先介绍S_NS算法。

(1) S_NS

S_NS算法应用于从输入信号中去除噪声成分,以提高信号的清晰度。

算法基本调用流程如下:

(a)初始化S_NS算法。先是通过tlka_aec_ns_get_version()得到算法的版本号,再通过tlka_ns_get_size()函数得到算法所需空间,从系统分配空间给S_NS算法,通过tlka_ns_init()进行S_NS算法初始化。

(b)每得到一个frame的单声道MIC数据送入tlka_ns_process_frame()算法执行处理,得到一个去除噪声信号的单声道音频信号。

S_NS算法调用流程

在SDK中可以通过宏使能S_NS算法生效,该宏开关控制S_NS算法所有S_NS的相关配置。该算法的宏总开关为0时,表示关闭S_NS算法,为1时表示打开该算法。

#define NS_ENABLE                                  0

(2) W_NS

W_NS算法应用于从输入信号中去除噪声成分,以提高信号的清晰度。目前降噪模块可以提供6~21db的降噪深度。

算法基本调用流程如下:

(a)初始化W_NS算法。先是通过tlka_w_ns_get_version()得到算法的版本号,再通过tlka_w_ns_get_size()函数得到算法所需空间,从系统分配空间给W_NS算法,通过tlka_w_ns_init()进行W_NS算法初始化。

(b)每得到一个frame的单声道MIC数据送入tlka_w_ns_process_frame()算法执行处理,得到一个去除噪声信号的单声道音频信号。

W_NS算法调用流程

在SDK中可以通过宏使能W_NS算法生效,该宏开关控制W_NS算法所有W_NS的相关配置。该算法的宏总开关为0时,表示关闭W_NS算法,为1时表示打开该算法。

#define  W_NS_EN                                  0

AGC

AGC算法应用于电平增益的调节,保证信号不会饱和。

算法基本调用流程如下:

(1)初始化AGC算法。先是通过tlka_agc_get_version()得到算法的版本号,再通过tlka_agc_get_size()函数得到算法所需空间,从系统分配空间给AGC算法,通过tlka_agc_init()进行AGC算法初始化。

(2)每得到一个frame的单声道MIC数据送入tlka_agc_process()算法执行处理,得到一个经过AGC处理后的单声道的音频信号。

AGC算法调用流程

在SDK中可以通过宏使能AGC算法生效,该宏开关控制AGC算法所有AGC的相关配置。该算法的宏总开关为0时,表示关闭AGC算法,为1时表示打开该算法。

#define SOFT_AGC_EN                                 0

系统死机调试方式

关于死机问题调试,目前提供两种方式:

方法1:

死机后,进入了MCU 的trap异常中断,通过输出打印MCU的PC指针位置,以及LA跳转返回地址,还有RSIC-V的MCAUSE/MVAL等,来判断导致死机的原因以及位置。

方法2:

死机后,并没有进入MCU的trap异常中断,或者进入trap异常中断后,并未获得有价值的信息,或者我们虽然知道了出现trap异常的原因,但是需要进一步找到根因所在等。我们根据常见死机问题的原因,我们可以尝试做如下操作:

(a) 如果出现死机时位置固定,我们可以通过流程控制法来确定是哪个操作导致的问题。比如定义一个volatile变量,在不同流程的位置赋值,从而判断是哪个流程出现的问题,再逐步缩小范围,确定问题根因所在。

(b) 如果出现死机是由于某些异常操作,篡改了指令,但是没法通过流程控制法来确定具体位置,那么可以通过在不同流程的节点对指定位置的指令进行检查来确定问题位置。比如某个特定的地址,指令被篡改了,那么可以通过函数check_target_address_code来触发trap异常(mcause=0x0b),从而结合PC和LA等来检查到底是哪一个过程更改了这个位置的指令。

(c) 如果我们发现IRAM(指令ram)被篡改了,导致执行指令时出现指令异常,那么我们也可以对IRAM区域进行校验运算,确定IRAM是否被篡改,如果被篡改就会触发trap异常(mcause=0x0b),再结合PC和LA等来确定具体的问题根因。

(d) 由于memcpy和memset函数,常常伴随一些指针操作,很容易出现越界等问题,导致指令异常,所以我们在这两个函数里面做了一些检查机制,如果参数有异常就会触发trap异常(mcause=0x0b),再结合PC和LA等来确定具体的问题根因。

TWS充电仓

充电仓、耳机与SY5500

充电仓、耳机与SY5500

如图,SY5500作为充电管理芯片与耳机主芯片之间进行交互,主要负责管理和保护耳机充电状态。两者通过I2C接口进行相关配置和状态查询。耳机与仓主要通过UART信令交互;充电仓会发送对应SY5500的模式切换信号以设计SY5500的正常状态。

对应的GPIO资源为:I2C部分GPIO_PB3、GPIO_PB2;UART 部分GPIO_PC7、GPIO_PC6;ADC部分GPIO_PB4;管控ADC采样GPIO_PC4;GPIO 唤醒 GPIO_PC5。

关于SY5500芯片的详细内容可参照对应芯片手册。

低电保护Flash操作

这里需要注意,当电源电压低于一定限度时,flash相关操作会产生错误风险,特别是设备处于OTA时,因此需要实时监督当前电压。低电压flash操作保护对应宏VBAT_ALRAM_FLASH_THRES_MV当前值设为2.5V。对应的低压flash检测的宏为BATT_TEST_DEMO和VBAT_ALRAM_FLASH_CHECK。

电量监管及充电

在耳机与仓的系统中,电量监管及充放电部分均在batt部分,主要包括batt.c、batt_UART.c和sy5500.c。可通过宏HEADSET_UI_EN和BATTERY_CODE对电源管理模块进行使能。

充电逻辑:耳机处于充电仓内且关盖为充电,否则为不充电。

App层中,可通过batt_init()和batt_task()两个接口进行电源管理模块的配置。

初始化流程

耳机电池满电一般会达到4.2 ~ 4.35V,3.5V及以下会播放提示音,低于3.3V会进入关机。这里考虑到充电电平的浮动,分为充电和非充电两个电平数组。耳机在充电仓内且关盖为充电,否则为不充电。

非充电的电平数组

充电的电平数组

SY5500芯片的相关配置在接口void sy5500_set_default_state()。

耳机在充电仓内且关盖状态下为充电,否则为不充电。手动开启充电仓后为非充电模式且耳机开机,充电仓通过pogo pin发送一个固定波形使SY5500处于TRX mode,即UART信令模式,紧接着发送对应的开仓命令。上电过程中,首先会进入初始化部分。

首先初始化对应模块和相关参数(下图标识1处);首次上电需要区分当前是否为PCBA状态(即产品第一次开机测试),PCBA模式下不会进入电量监管状态,但非PCBA即正常状态下会进入充电检测和电量监管状态(下图标识2处);上电后快速判断当前电量,电量正常,保存当前电量并默认耳机处于充电仓内(下图标识4处);否则,如果电量过低,则退出TRX模式并进入deep sleep模式(下图标识3处)。

上电初始化

上电初始化

这里主要分为三个部分:充电状态检测、power on电池电量检测和低电处理模块。其接口分别对应于:sy5500_charge_status_handler()、intbatt_power_on_level_check()和batt_deep_handler()。

(1)上电查询SY5500状态时,由于开盖上电后会脱离充电状态,这里除了TRX模式作为正常用于仓和耳机之间的交互状态之外,其他情况均属于非正常状态,需要reset相关参数并进入deep sleep处理(下图标识1处)。上电为TRX状态时,则认为此次开机状态为正常上电,此时会快速采集当前电量(下图标识2处)。

充电状态检测

充电状态检测

该接口检测对应SY5500的相关状态,并仅会在上电时引用。

(2)上电首次快速电量采集是一个延时过程,每200us进行采集一次,需要采集足够的电量值(16次),会更新当前电量值并保存(下图标识1、3处);如果打开宏VBAT_ALRAM_FLASH_CHECK则开启了flash低电平警告检测功能(下图标识2处);如果为TWS模式即双耳状态下会比较出一个较低的电量作为当前电量(下图标识4处)。

power on电池电量检测

(3)当满足某些条件进入deep sleep状态时,会进入下图接口;默认的deep sleep唤醒源为pad唤醒,对应的GPIO为GPIO_PC5(下图标识1处);如果此时为5V充电状态下,则增加定时唤醒源,同时如果充满情况下唤醒周期为30s,未充满唤醒周期为5s(下图标识2,3处);非充电状态下关闭电量采集并更新充电状态(下图标识4处);如果为TRX模式或者处于standby模式并收到来自仓的命令时,会于100ms后醒来(下图标识5处)。

低电处理模块

完整的开关机流程可参照以下逻辑分析仪时序图:

开机及正常状态下UART时序图:

开机及正常状态下UART时序图

关机及充电状态下UART时序图:

关机及充电状态下UART时序图

如上图,关机时会退出SY5500的TRX mode, 并计时进入强制关机;关机并在仓内充电时,耳机会每隔5s左右的时间唤醒一次,用于检测充电时的温度等相关状态,以保护SY5500芯片和电路安全。

实时电量采集、出入仓及充电

目前电量采集和充电状态监管处理均在main_loop中处理,对应接口batt_task()。该接口主要分为:出入仓状态管理,电量采集更新,低电量处理,充电状态处理,与SY5500芯片交互和与仓之间的UART信令交互,包含下列接口。

batt_task

当处于TWS模式下的master进行scan时,需要与slave同步电池电量,batt_tws_check_scan_state();

出入仓状态对应接口batt_in_out_box_check()。耳机入仓状态主要有两个依据:一是充电状态; 二是收到UART信令为有效值(下图标识1处);出仓则通过超时机制判断(下图标识3处)。耳机在仓内的speaker是静音的,只有出仓才会打开(下图标识2处)。

耳机出入仓状态

电量监管部分对应接口batt_level_supervise_task(),每隔500ms采集一次电量,这里与上次电量进行对比,如果为充电状态只能增加,否则只能减少(下图标识1处)。一旦电量更新之后需要同步TWS状态下的主副耳电量(下图标识2处);如果此时电量太低会进行handover处理退出TRX模式并开始关机倒计时(下图标识3处)。

电量采集更新

当电平较低处于3.5V以内且尚未进入power off时,耳机会播放低电量提示音,并闪烁LED开始计时15min(下图标识1处),之后再次播放一次提示音,LED闪烁并计时15min后退出TRX模式进入关机倒计时(下图标识2处)。

低电量处理

低电量处理

TWS模式下当耳机放回仓内时,该耳机会进入handover,并进入关机倒计时。对应的接口为batt_charge_reboot_handler()。

芯片B91与SY5500之间通过I2C进行交互,当读取到SY5500芯片的温度超过运行范围会进入ship mode。此过程对应接口batt_sy5500_task()。

工作状态 温度区间 保护行为
充电阶段 T < 0°C 关闭充电
0°C < T < 10°C 充电电流减半
10°C < T < 45°C 正常充电
45°C < T < 60°C 浮充电压降至4.05V
60°C < T 关闭充电
放电阶段 T < -10°C 关闭放电
-10°C < T < 60°C 正常放电
60°C < T 关闭放电

耳机与仓之间通过UART进行通信,信令主要包括下图内容,一旦耳机收到来自仓的有效UART信令,则认为耳机处于仓内。

UART信令

例如下图中的UART时序信息:

开仓命令:From box 0x5555

耳机状态:From ear 0x55a51812

UART时序信息