TWS_1T2_SDK框架说明
TWS 1T2 SDK-框架说明
1.开发背景
TWS耳机功能需求比较固定,常用功能、 UI可通过工具配置,减少开发成本。音频数据处理流程可通过流程图串接方式实现,不用修改SDK。
2.设计理念
以工具配置为主,代码开发为辅, SDK 能够提供的参数、功能等都可以放到工具上提供配置。 SDK 提供的事件,状态, API 都可以开放到情景配置中组合调用。音频处理流程图形化,将数据处理流程抽象成独立的功能节点,可以通过工具自由组合, 提高调音效率。
3.配置工具简介
配置工具通过新建工程的方式管理方案, 集配置、编译、调音、在线更新等功能为一体,提供集成式的开发环境。配置工具的安装路径不能带有空格,新建的项目存放路径也不能有空格,否则会导致文件复制不完整,无法编译。
首次使用配置工具需要登陆才能下载SDK,创建工程。杰理内部员工可通过钉钉扫码方式登陆,客户需要找助理拿二维码,扫码绑定后可通过微信扫码登陆。
下载的SDK存放在安装目录的standard文件夹下,创建工程时会复制部分配置文件和 SDK到指定的工程目录。
SDK补丁更新:
当有新的补丁时,工具会将补丁文件下载到standard 目录下对应版本的patch 目录。然后自动给standard 目录下打上补丁。
创建的工程可通过点击 ”项目/主页”,点击补丁按钮,选择”自动合并”的方式打补丁,如果文件有冲突会显示红色并提示,单击即可打开对比软件,然后手动合并。合并完成后会在工程目录/SDK/ . patch 目录下生成patch_xx文件已标记SDK 的补丁版本。
编译下载:
工具的编译功能是通过启动SDK 目录下的 make_prompt. bat,然后执行 make-j命令实现,所以如果有新加文件需要编译,需要自己添加到Makefile文件
导入导出配置:
当SDK有较大改动,无法通过补丁的方式更新时,会发布新的版本,这时如果想使用旧工程的配置,可通过 项目->导出配置 功能将配置信息导出到文件中,然后在新版本的工程上通过 项目->导入配置 功能将配置信息导入,如果有冲突会有提示,根据提示信息修改冲突的地方即可
4. SDĸ使用说明
4.1 SDK 目录说明
apps
|
common 各个方案的共用模块
|
app_mode_manager app模式管理
|
clock_manager 自适应时钟模块
|
device 设备驱动
|
scene_manager 情景模式解析模块
earphone TWS耳机方案
|
audio 耳机特有的音频相关代码( aec, tone, volume)
|
battery 电池和充电
|
ble ble广播
|
board 板级相关配置(电源,IO, key)
|
log_config 库的功能配置和打印开关
|
message/adapter 各模块消息发送适配层,耳机方案发送给app_core |
mode 模式相关代码
|
movable 代码动态加载相关配置
|
scene 情景配置各模块能力集合
| audio |
framework | 开放源码的音频框架适配层 | | --- | --- | | |
interface | 音频框架提供的接口 |
interface 各个库提供的接口
4.2 按键驱动
改动说明
原来SDK的key_driver.c为了适应耳机,音箱,手表等方案对按键消息的不同需求,包含很多控制宏,而且功能复杂,除了基本消息外,还要处理多击、组合键等操作,随着要适配的方案越加越多,变得更加难以修改。
新版可视化key_driver.c只保留单击,长按等基本操作,各方案可跟进自身的业务需求,通过消息字段中增加的时间戳信息实现多击、组合键等操作,以此可以保证 key_driver.c的简洁稳定,各方案自己的修改也不会影响其它方案。
相关代码
按键驱动公共代码
common/device/key/
耳机方案扩展代码
earphone/messapge/adapter/key.c
耳机方案按键消息说明:
原始消息:
struct key_event {
value; //键值
event; //动作
}
TWS转发:
App_core收到原始的按键消息后,会分发给注册了按键消息的接口,bt 模式下收到后会通过get_key_remap_table()函数获取映射表,将按键消息映射成APP消息,然后通过tws 的发送函数发送给对方。默认的映射表定义在 key_ablity.c 中定义,通过APP_KEY_MSG_REMAP()完成映射。
如果要获取本地的原始的按键消息,可以通过下面接口注册:
static int key_msg_demo_handler(int *msg)
{
struct key_event event = (struct key_event)msg;
switch (event->value) {
}
}
APP_MSG_HANDLER(key_msg_demo_entry) = {
.owner = 0xff,
.from = MSG_FROM_KEY,
. handler = key_msg_demo_handler,};
如果要获取包含TWS 的转发的按键消息,可以通过下面接口注册:
static int key_msg_demo_handler(int *msg)
{
if (!APP_MSG_FROM_KEY(msg [0])) {
return 0;
}
int key_value = APP_MSG_KEY_VALUE(msg [0]);
int key_action = APP_MSG_KEY_ACTION(msg [0]);
int from_tws = msg [ 1];
// 添加处理代码
}
APP_MSG_HANDLER(key_msg_demo_entry) = {
.owner = 0xff,
.from = MSG_FROM_APP,
. handler = key_msg_demo_handler,};
可以参考earphone/mode/bt/key_msg_handler.c
4.3 消息机制
改动说明:
原SDK消息机制采用统一数据格式和接口传递,随着要适配的方案越来越多, struct sys_event 结构体变得越来越大,消息的分发方式为给任务发送taskq,不太适合每个模块自己注册,这会导致各个模块的消息处理函数集中在一起,然后需要很多宏来包住,不利于模块之间的解耦。
新版SDK对信息机制进行了简化处理,去掉了对sys_event 的依赖,不在使用统一的接口分发。各个模块只需声明自己的发送消息的接口,具体的实现改成方案自 己 实 现, 以 TWS 耳 机 方 案 为 例, 各 模 块 发 送 消 息 的 接 口 在 app/earphone/msessage/adapter 目录下实现,先统一发给app_core任务,在由 app_core任务负责分发, 分发机制为函数列表遍历调用,开销就比较小,适合各个模块分别注册, 以此实现各模块代码独立。
注册方式:
APP_MSG_HANDLER(msg_demo_entry) = {
.owner = 0xff,
.from = MSG_FROM_APP,
. handler = msg_demo_handler,
};
APP_MSG_PROB_HANDLER(msg_demo_entry) = {
.owner = 0xff,
.from = MSG_FROM_APP,
. handler = msg_demo_handler,
};
参数说明:
owner: 表示在什么模式下有效,0xff表示所有模式下都可以接收消息
from: 表示接收哪个模块发出的消息
handler: 消息的处理函数
消息分发:
static void app_task_loop(void *p)
{
int msg [ 16];
struct app_mode *mode;
const struct app_msg_handler *handler;
while (1) {
//通过os_taskq_pend()获取消息
app_get_message(msg, ARRAY_SIZE(msg));
//消息截获,返回 1表示中断消息分发
int abandon = 0;
for_each_app_msg_prob_handler(handler) {
if (handler->from == msg [0]) {
abandon = handler->handler(msg + 1);
if (abandon) {
break;
}
}
}
if (abandon) {
continue;
}
//当前模式消息处理
mode = app_get_current_mode();
if (mode) {
mode->ops->msg_handler(msg);}
//消息继续分发
for_each_app_msg_handler(handler) {
if (handler->from != msg [0]) {
continue;
}
//当前模式和注册的模式相同,不在调用,防止重复处理 if (mode && mode->name == handler->owner) {
continue;
}
handler->handler(msg + 1);
}
}
}
参考代码:
earphone/message/adapter
earphone/app_main.c
earphone/include/app_msg.h
4.4 APP模式管理
APP模式采用静态注册的方式定义:
static const struct app_mode_ops bt_mode_ops = {
.enter = bt_mode_init,
.try_exit = bt_mode_try_exit,
.exit = bt_mode_release,
. msg_handler = bt_mode_msg_handler,};
REGISTER_APP_MODE(bt_mode) = {
. name = APP_MODE_BT,
. index = 1,
.ops = &bt_mode_ops,
};
字段说明:
. name : 模式的名字
. index: 模式的顺序索引,系统初始化时会按次索引从小到大排列mode 用于模式向前或向后顺序切换
.enter : 进入模式的初始化接口
.exit: 退出模式的释放资源接口
.try_exit: 检测是否可以退出的接口,返回0表示可以退出,非0表示不能退出
. msg_handler: 模式的消息处理接口
相关代码:
common/app_mode_manager.c
earphone/mode/bt/earphone.c
earphone/mode/idle/idle.c
4.5 蓝牙模式
文件说明:
bt_slience_detect.c TWS 1T2 A2DP能量检测
bt_tws.c TWS初始化,连接,断开消息处理
earphone.c 蓝牙初始化和配置
low_latency.c 低延时模式接口
poweroff.c TWS关机流程
sniff.c 经典蓝牙进退sniff流程
tone.c TWS,手机蓝牙断连提示音播放
tws_a2dp_play.c TWS 1T2 高级音频播放
tws_dual_conn.c TWS 1T2 配对逻辑
tws_phone_call.c TWS 1T2 通话相关事件处理
改动说明:
1.原earphone.c 的功能进行了拆分,将连接、sniff、通话、播歌等功能独立出来
2.原bt_tws.c 中的配对逻辑和earphone.c 中的断连逻辑统一归纳到 tws_dual_conn.c 中处理
3.按键、LED 的相关操作在配置工具的 “情景配置”中实现。
4. TWS 的从机协议栈不在运行,APP层从机不需要和主机同时调用其接口以保持状态同步,相关事件和状态由主机转发给从机,从机加入的情况下不会收到蓝牙连接相关的消息
配对流程:
1.开机先连接tws,默认超时 6s
2.如果没有连接记录,开可发现,可连接,连上第一台手机后,会继续打开可发现,可连接,第二台手机可以搜索连接,默认时间 120s
3.如果有连接记录,先回连手机1, 回连时间默认8s
4.如果手机1 回连成功,没有第二台配对记录,会继续打开
可发现,可连接,第二台手机可以搜索连接,默认时间 120s
5.如果手机1 回连成功,有第二台配对记录就回连手机2 8s,如果回连不成功,就开可连接6s,超时后在回连手机2,一直重复此过程,直到总的回连时间超时,然后就只开可连接
6.如果手机1没有回连成功, 就开可发现,可连接,默认时间6s,超时后回连手机2 8s,如果手机2也没有回连成功,在开可发现,可连接6s,超时后回连手机1,一直重复此过程,直到总的回连时间超时,然后只开可发现,可连接
可发现可连接
开机120s后
关闭
连上1台手机
可发现可连接
开机120s后
关闭

关可发现
可连接
0个
成功
开机
连接
TWS
配对记
录
回连手
机1 8s
开可发现
可连接6s
回连手
机1 8s
多个
1个
回连手
机2 8s
关可发现
可连接
成功
成功
失败
回连手
机2 8s

失败
开可连接
6s
回连手机2
开可连接6s



失败

4.6 自适应时钟
鉴于原SDK不同情况下需要配置不同的系统频率,导致频率相关的控制宏过多,调用的地方也多,维护起来非常麻烦,新版SDK 引入自适应时钟策略,调用clk_set()函数会触发系统将系统频率调到最大,然后通过统计操作系统跑idle任务的时间,自动往下降。自适应模块默认最小时钟为24M,外部可通过clock_alloc()接口在叠加一个频率作为最小频率,系统降到最小频率后会停止检测。
参考代码:
common/clock_manager/clock_manager.c
earphone/audio/jlstream_event_handler.c
4.7 情景配置
新配置工具引入情景配置功能,将SDK 的能力开放到工具中, 以添加卡片的方式实现功能的联动,可以替代一部分的代码开发工作。
模块的情景能力通过下面的宏来注册:
REGISTER_SCENE_ABILITY(btstack_ability) = {
. uuid = UUID_BT,
.event = btstack_event_table,
.state = btstack_state_table,
.action = btstack_action_table,};
字段说明:
. uuid : 模块的 uuid,在ability_uuid. h 中定义,需要和工具端保持一至
.event: 提供的事件列表
.state: 提供的可查询的状态列表
.action: 提供的操作列表
每个模块可根据自身的特点,选择性的实现。
执行流程:
情景模块注册对应的消息,当有消息发生时,通过调用scene_mgr_event_match 函数将消息传入,scene_mgr_event_match()中会解析工具生成的scene. bin文件,从中读取每个情景卡片,查找匹配的事件,如果事件匹配成功,则遍历卡片的条件列表,当所有条件都成立,则调用卡片的动作列表函数。
情景配置的log格式为:
[scene] : event uuid/subid state: uuid/subid/param action: uuid/subid/param
如果没有打印action相关内容,则最后一个state 即为不成立的条件
使用 配置工具->工具->串口输出工具 作为打印终端,可将情景的 log翻译成对应
的中文描述,示例:
[scene]: event: KEY_POWER/单击 state: 蓝牙/非通话状态 action: 蓝牙/播放暂停
参考代码:
common/scene_manager/scene_manage.c
earphone/scene/
4.8 音频框架
原 SDK 音频系统都是以编解码为中心,通过编解码来驱动整个数据流的传递,通过各个模块的 prob, handler, post 函数将各个模块串接在一起,好处是模块比较少的时候流程看起来还算清晰,坏处是当 SDK 需要兼容不同的方案、或者模块变多之后,流程的串接就比较混乱,想要修改数据流就变得非常麻烦。
新版SDK在之前的基础上,开发出了以数据流为中心的jlstream音频框架,框架层通过解析stream. bin文件中的节点连接信息创建出整条数据流,由专门的任务驱动数据的传递,框架层同时还提供了参数协商机制、多路音频共存策略,结构图如下:
如下图所示的提示音播放器的实现示例:
static struct jlstream *stream;
int play_tone_file(const char *file_name)
{
// 通过pipeline 的 uuid和起始节点的 uuid创建数据流
stream = jlstream_pipeline_parse(0x7674, NODE_UUID_TONE);
// 注册事件回调
jlstream_set_callback(stream, NULL, tone_player_callback);
// 设置场景,用于各个节点区分不同的场景,框架会用来做共存策略 jlstream_set_scene(stream, STREAM_SCENE_TONE);
// 设置共存方式, STREAM_COEXIST_AUTO 内部自动决策
jlstream_set_coexist(stream, STREAM_COEXIST_DISABLE) //抢占方式
// 打开文件
void *file = resfile_open(file_name);
// 启动
jlstream_set_dec_file(stream, file, &tone_file_ops);
}
接口层的API采用最小化原则进行设计,每个接口都设计的尽可能的简单,不同功能提供不同的接口,避免为了适应多种需求而设计超大接口
参考代码:
audio/interface/
audio/framework/ interface/media/framework/