经典蓝牙 BR/EDR
经典蓝牙 BR/EDR(Basic Rate / Enhanced Data Rate)是 AC79 系列 AIoT SDK 中面向音频设备的传统蓝牙能力集合,覆盖 A2DP 音乐播放、HFP 通话、设备搜索、回连与发射器(Emitter)等完整链路。本文档基于 apps/common/example/bluetooth/edr/bt_music/ 下的 EDR Demo 源码,讲解经典蓝牙在应用层的实现机制、核心数据结构、控制流程与扩展方式。
Purpose and Scope
本文档介绍 SDK 中经典蓝牙 BR/EDR 子系统在**应用层(App Layer)**的实现,具体包括:
- EDR Demo 的框架入口与编译开关(
USE_EDR_DEMO/CONFIG_BT_ENABLE/TCFG_USER_EMITTER_ENABLE) - 蓝牙应用状态管理结构
struct app_bt_opr及其状态字段 - 可发现/可连接(Discoverable/Connectable)控制逻辑
bt_wait_phone_connect_control() - 经典蓝牙发射器(Emitter)的搜索与连接流程
- A2DP 音乐解码与 HFP 通话在应用层的音频通路
- 与底层协议栈的交互接口(
btstack/avctp_user.h、classic/hci_lmp.h、event/bt_event.h)
以下内容不在本文档范围内,由同目录其他页面负责:
- BLE(低功耗蓝牙)相关能力,参见 BLE 系列文档(如
apps/common/example/bluetooth/ble/下的 GATT Client/Server 示例) - 底层控制器(BT Controller)HCI 命令实现与射频链路层细节
- 第三方协议(如 RCSP 私有协议)及其
bt_trans_data透传通道
Overview
在 AC79 SDK 中,经典蓝牙 BR/EDR 的完整能力由三层构成:**蓝牙控制器(BT Controller)**负责射频收发与链路管理(ACL/SCO 链路、查询 Inquiry、寻呼 Page);**协议栈(BTStack)**实现 L2CAP、SDP、AVDTP/AVCTP、A2DP、HFP、SPP 等 Profile;应用层(本文档重点)通过 user_send_cmd_prepare() 向协议栈下发控制命令,并通过 bt_event 事件回调驱动状态机。
EDR Demo 位于 apps/common/example/bluetooth/edr/bt_music/,是一个"音乐播放 + 电话"二合一参考实现:
main.c:应用主逻辑,持有全局状态app_bt_hdl,管理连接、回连、音量、通话与音乐切换;bt_decode.c:音频解码线程,对接 A2DP 媒体流(SBC/MPEG 编解码)与 HFP 语音(eSCO)数据;bt_emitter.c:发射器模式(可选,TCFG_USER_EMITTER_ENABLE开启),使本设备作为音源向其他蓝牙音箱发射音乐。
设计意图:通过"事件驱动 + 命令下发"的架构,把复杂的 BR/EDR 链路管理收敛到底层协议栈,应用层只维护一份精简的状态(app_bt_opr),从而降低多 Profile 并发(A2DP 与 HFP 同时激活)时的耦合复杂度。
Architecture
flowchart TD
subgraph sg_App["应用层 (EDR Demo)"]
Main["main.c<br/>app_bt_hdl 状态机"]
Decode["bt_decode.c<br/>音频解码 (A2DP/HFP)"]
Emitter["bt_emitter.c<br/>发射器模式"]
end
subgraph sg_Stack["协议栈层 (BTStack)"]
Avctp["avctp_user.h<br/>AVCTP/AVDTP 用户接口"]
A2dp["a2dp_media_codec.h<br/>A2DP 媒体编解码"]
HciLmp["classic/hci_lmp.h<br/>HCI/LMP 链路控制"]
BtEvent["event/bt_event.h<br/>蓝牙事件"]
end
subgraph sg_OS["系统服务"]
Server["server_core.h<br/>音频服务"]
SysCfg["syscfg/syscfg_id.h<br/>VM 配置存储"]
Timer["system/timer.h<br/>定时器"]
end
Main -->|"user_send_cmd_prepare()"| Avctp
Main -->|"USER_CTRL_* 命令"| HciLmp
Decode --> A2dp
Emitter -->|"USER_CTRL_SEARCH_DEVICE / START_CONNEC_VIA_ADDR"| HciLmp
Avctp -->|"bt_event 回调"| Main
Decode --> Server
Main --> SysCfg
Main --> Timer
Emitter --> Timer
架构说明:
- main.c(状态中枢):
static struct app_bt_opr app_bt_hdl是唯一的应用级状态体,通过__this宏访问。所有 Profile 事件(来电、音乐开始/暂停、连接建立/断开)最终都汇聚到这里,由它决定下一步动作(回连、切换音频、更新 UI)。 - bt_decode.c(音频通路):编译期通过
BT_MUSIC_DECODE与BT_PHONE_DECODE宏分别使能音乐解码与通话解码;解码器对接earphone_a2dp_audio_codec_open()等音频服务接口,输出到server_core音频服务器。 - bt_emitter.c(发射器):把本机变成"音源",先发起 Inquiry 搜索周边设备(
USER_CTRL_SEARCH_DEVICE),再通过USER_CTRL_START_CONNEC_VIA_ADDR按 MAC 地址建链。 - 协议栈交互:应用层不直接触碰射频,所有动作都转化为
USER_CTRL_*命令(如USER_CTRL_WRITE_SCAN_ENABLE、USER_CTRL_PAGE_CANCEL)下发到avctp_user/hci_lmp,结果以bt_event回调返回。这种"命令-事件"模式是 AC79 蓝牙框架的标准交互范式。
核心实现详解
应用状态机:struct app_bt_opr
main.c 定义了整个经典蓝牙应用的核心状态结构,所有运行期状态都浓缩在这一结构体中:
struct app_bt_opr {
//phone
u8 phone_ring_flag: 1;
u8 phone_num_flag: 1;
u8 phone_income_flag: 1;
u8 phone_call_dec_begin: 1;
u8 phone_con_sync_num_ring: 1;
u8 phone_con_sync_ring: 1;
u8 emitter_or_receiver: 2;
u8 media_play_flag : 1;
u8 call_flag : 1; // 1-由于蓝牙打电话命令切回蓝牙模式
u8 exit_flag : 1; // 1-可以退出蓝牙标志
u8 enable : 1;
u8 emitter : 1;
u8 mute : 1;
u8 siri_stu; // ios siri
u8 call_volume;
u8 media_volume;
u8 inband_ringtone;
u8 last_call_type;
u8 auto_connection_addr[6];
u8 income_phone_num[30];
u8 income_phone_len;
u16 phone_timer_id;
u16 poweroff_timer_id;
int auto_connection_counter;
int auto_connection_timer;
int sniff_timer;
void *dec_server;
};
Source: main.c
设计要点:
- 位域压缩:电话侧 6 个标志与发射/接收角色共用 1 字节(
phone_ring_flag到emitter_or_receiver),媒体/通话/退出等标志再占 1 字节。这种紧凑布局说明该结构会在中断/低功耗场景下被频繁读写,用位域节省 RAM 并方便原子性判断。 - 双角色并存:
emitter_or_receiver(2 bit,可表示 4 种状态)与emitter标志配合,支持"接收器"(默认音箱)与"发射器"(音源)两种角色切换;call_flag专门记录"因蓝牙电话命令切回蓝牙模式",用于音频路由回切。 - 回连状态:
auto_connection_addr[6]保存上次连接的设备地址,auto_connection_counter/timer驱动超时回连(TIMEOUT_CONN_TIME = 30秒),poweroff_timer_id管理关机流程。 - Siri 支持:
siri_stu字段为 iOS Siri 语音助手交互保留状态,说明该 Demo 覆盖了手机语音助手的经典蓝牙场景。
可发现 / 可连接控制:bt_wait_phone_connect_control()
这是经典蓝牙"设备可见性"的核心逻辑,根据当前连接数动态决定扫描(Inquiry Scan)与寻呼(Page Scan)开关:
static void bt_wait_phone_connect_control(u8 enable)
{
if (!__this->enable && enable) {
return;
}
if (enable) {
log_i("is_1t2_connection:%d \t total_conn_dev:%d\n", is_1t2_connection(), get_total_connect_dev());
if (is_1t2_connection()) {
/*达到最大连接数,可发现(0)可连接(0)*/
user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_DISABLE, 0, NULL);
user_send_cmd_prepare(USER_CTRL_WRITE_CONN_DISABLE, 0, NULL);
} else {
if (get_total_connect_dev() == 1) {
/*支持连接2台,只连接一台的情况下,可发现(0)可连接(1)*/
user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_DISABLE, 0, NULL);
user_send_cmd_prepare(USER_CTRL_WRITE_CONN_ENABLE, 0, NULL);
} else {
/*可发现(1)可连接(1)*/
user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_ENABLE, 0, NULL);
user_send_cmd_prepare(USER_CTRL_WRITE_CONN_ENABLE, 0, NULL);
}
}
} else {
user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_DISABLE, 0, NULL);
user_send_cmd_prepare(USER_CTRL_WRITE_CONN_DISABLE, 0, NULL);
}
}
Source: main.c
设计意图:
- 多连接(1T2)支持:
is_1t2_connection()表示是否达到最大连接数(本 Demo 支持同时连接 2 台手机)。达到上限后同时关闭可发现与可连接,避免手机反复尝试入链;只连 1 台时仍开放可连接(允许第二台接入)但关闭可发现(不再广播自身),这是功耗与可用性的折中。 - 统一命令出口:所有开关都通过
user_send_cmd_prepare(USER_CTRL_*, 0, NULL)异步下发,由协议栈串行处理,避免应用层直接操作寄存器带来的竞态。 - 使能保护:
if (!__this->enable && enable)防止在蓝牙未使能时误开扫描。
发射器(Emitter)模式
bt_emitter.c 在 TCFG_USER_EMITTER_ENABLE 与 USE_EDR_DEMO 双重宏控制下编译,实现"本机作为音源"的完整搜索-连接流程。
设备搜索
void bt_search_device(void)
{
if (bt_search_busy) {
//return;
}
user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_DISABLE, 0, NULL);
user_send_cmd_prepare(USER_CTRL_WRITE_CONN_DISABLE, 0, NULL);
read_name_start = 0;
bt_search_busy = 1;
u8 inquiry_length = 20; // inquiry_length * 1.28s
user_send_cmd_prepare(USER_CTRL_SEARCH_DEVICE, 1, &inquiry_length);
log_i("bt_search_start\n");
}
Source: bt_emitter.c
关键点:
- 搜索前先关闭本机扫描/可连接,避免自身被其他设备发现(发射器模式只主动连接别人)。
inquiry_length = 20,即 Inquiry 持续20 × 1.28s ≈ 25.6秒,bt_search_busy标志防止重复发起搜索。- 搜索限制模式通过
SEARCH_LIMITED_MODE宏选择,可选按 BD_ADDR(SEARCH_BD_ADDR_LIMITED)、按设备名(SEARCH_BD_NAME_LIMITED,默认)、自定义过滤或不过滤,对应struct remote_name(含crc/addr[6]/name[32])与struct inquiry_noname_remote(链表节点,含rssi、class)两套结果容器。
按地址建链
void emitter_bt_connect(u8 *mac)
{
if (emitter_or_receiver != BT_EMITTER_EN) {
return;
}
while (hci_standard_connect_check() == 0x80) {
//wait profile connect ok;
if (get_curr_channel_state()) {
break;
}
os_time_dly(10);
}
////断开链接
if (get_curr_channel_state() != 0) {
user_send_cmd_prepare(USER_CTRL_POWER_OFF, 0, NULL);
} else {
if (hci_standard_connect_check()) {
user_send_cmd_prepare(USER_CTRL_PAGE_CANCEL, 0, NULL);
user_send_cmd_prepare(USER_CTRL_CONNECTION_CANCEL, 0, NULL);
}
}
/* if there are some connected channel ,then disconnect*/
while (hci_standard_connect_check() != 0) {
//wait disconnect;
os_time_dly(10);
}
user_send_cmd_prepare(USER_CTRL_START_CONNEC_VIA_ADDR, 6, mac);
}
Source: bt_emitter.c
设计意图:
- 先复位再建链:连接新设备前必须确保链路干净——若有 Profile 正在建链(
hci_standard_connect_check() == 0x80)先等待完成;已有连接则发USER_CTRL_POWER_OFF断开;若处于寻呼中则先USER_CTRL_PAGE_CANCEL+USER_CTRL_CONNECTION_CANCEL取消,最后轮询等待链路完全断开(while+os_time_dly(10))才发起USER_CTRL_START_CONNEC_VIA_ADDR。这种"串行化"策略保证了单射频(BR/EDR 半双工)下不会出现多路寻呼竞争。 - 命令语义区分:
USER_CTRL_POWER_OFF用于彻底断开已有连接(含 RF 关断),USER_CTRL_PAGE_CANCEL仅取消正在进行的寻呼,二者针对不同链路状态,体现了协议栈命令的精细粒度。
停止搜索与状态查询
u8 bt_search_status(void)
{
return bt_search_busy;
}
u8 bt_emitter_role_get(void)
{
return emitter_or_receiver;
}
void bt_emitter_stop_search_device(void)
{
user_send_cmd_prepare(USER_CTRL_INQUIRY_CANCEL, 0, NULL);
}
Source: bt_emitter.c
这三个接口分别供 UI/按键层查询搜索忙状态、查询当前角色、以及通过 USER_CTRL_INQUIRY_CANCEL 中止 Inquiry,是应用层与发射器模块的标准门面(Facade)。
音频解码通路:bt_decode.c
bt_decode.c 通过 BT_MUSIC_DECODE(音乐)与 BT_PHONE_DECODE(通话)两个宏分别启用 A2DP 与 HFP 解码路径,并定义 A2DP 编解码器类型:
#define A2DP_CODEC_SBC 0x00
#define A2DP_CODEC_MPEG12 0x01
#define A2DP_CODEC_MPEG24 0x02
Source: bt_decode.c
- 编码类型与
a2dp_media_codec.h的媒体流协商结果对应,解码线程依据类型分发到对应解码器。 - 解码器通过外部接口接入音频服务器:
earphone_a2dp_audio_codec_open(int media_type, u8 volume)打开 A2DP 解码,phone_call_begin(void *priv, u8 volume)/phone_call_end()管理通话解码,a2dp_media_get_total_buffer_size()查询缓冲水位。 - 后台静音检测定义了三个关键时间窗:
SBC_FILTER_TIME_MS = 2000(后台音频过滤时间)、SBC_ZERO_TIME_MS = 500(静音多久认为已退出)、NO_SBC_TIME_MS = 100(无音频时间),用于在无有效音频数据时快速关断解码器以省电。 - 通话路径还检查
get_esco_coder_busy_flag()/get_esco_busy_flag(),避免 eSCO 语音编解码器被音乐播放占用(A2DP 与 HFP 共享音频硬件的互斥保护)。
核心流程
经典蓝牙完整生命周期(接收器模式)
sequenceDiagram
participant App as 应用层 main.c
participant Stack as BTStack (avctp_user/hci_lmp)
participant Phone as 手机
participant Dec as bt_decode.c
participant Aud as 音频服务器
Note over App: 上电/使能
App->>Stack: user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_ENABLE / CONN_ENABLE)
Stack-->>App: bt_event (可发现/可连接)
Phone->>Stack: Inquiry/Page 请求
Stack-->>App: 连接建立事件
App->>App: bt_wait_phone_connect_control() 更新扫描/连接开关
Note over App: A2DP 音乐播放
Phone->>Stack: AVDTP 媒体流 (SBC)
Stack->>Dec: 媒体数据回调
Dec->>Dec: 按 A2DP_CODEC_* 分发解码
Dec->>Aud: earphone_a2dp_audio_codec_open(media_type, volume)
Aud-->>App: 解码完成事件
Note over App: HFP 来电
Phone->>Stack: eSCO/Ring 事件
Stack-->>App: phone_ring_flag 置位
App->>Dec: phone_call_begin(priv, volume)
Dec->>Aud: 语音编解码 (eSCO)
App->>Stack: 通话结束 -> phone_call_end()
发射器模式搜索-连接流程
flowchart TD
Start([触发搜索]) --> Disable["USER_CTRL_WRITE_SCAN_DISABLE<br/>USER_CTRL_WRITE_CONN_DISABLE"]
Disable --> Inquiry["USER_CTRL_SEARCH_DEVICE<br/>inquiry_length = 20 x 1.28s"]
Inquiry --> Found{"搜索到设备?"}
Found -->|"是"| Select["UI 选择目标设备 (按名字/地址过滤)"]
Found -->|"否/超时"| Stop["USER_CTRL_INQUIRY_CANCEL<br/>bt_search_busy = 0"]
Select --> Check{"当前链路状态"}
Check -->|"Profile 建链中 (0x80)"| Wait["等待建链完成<br/>os_time_dly(10) 轮询"]
Wait --> Check
Check -->|"已有连接"| Off["USER_CTRL_POWER_OFF 断开"]
Check -->|"寻呼中"| Cancel["USER_CTRL_PAGE_CANCEL<br/>USER_CTRL_CONNECTION_CANCEL"]
Off --> Drain["轮询等待链路完全断开"]
Cancel --> Drain
Drain --> Connect["USER_CTRL_START_CONNEC_VIA_ADDR(mac, 6)"]
Connect --> Done([建链成功, 进入发射播放])
Stop --> Done
流程要点:
- 上电回连:开机后应用读取 VM 中保存的
auto_connection_addr[6](通过get_remote_device_info_from_vm()),在POWERON_AUTO_CONN_TIME(18 秒,取 6 的倍数以配合协议栈超时粒度)窗口内自动回连上次设备;若回连失败,则在TIMEOUT_CONN_TIME(30 秒)后再次触发,由auto_connection_counter控制尝试次数。 - 连接数驱动的可见性:每建立/断开一台设备都会重新调用
bt_wait_phone_connect_control(),在 0/1/2 台连接三个档位间切换 SCAN/CONN 开关,实现"第二台手机可接入、第三台不可见"的 1T2 策略。 - 音频路由:音乐与来电通过
call_flag、media_play_flag协调:来电时暂停音乐(BT_INBAND_RINGTONE决定是否播放手机彩铃),挂断后依据last_call_type恢复音乐或进入静默。 - 发射器建链的确定性:
emitter_bt_connect()通过while轮询保证任意历史状态下链路都被复位到"无连接",再发起USER_CTRL_START_CONNEC_VIA_ADDR,使按地址建链的时序完全确定。
使用示例
示例 1:使能/关闭经典蓝牙并控制可见性
/* 使能蓝牙:开启可发现与可连接,等待手机配对 */
__this->enable = 1;
bt_wait_phone_connect_control(1);
/* 关闭蓝牙:同时关闭可发现与可连接 */
bt_wait_phone_connect_control(0);
Source: main.c
示例 2:发射器搜索周边蓝牙设备
/* 发起 20 x 1.28s 的设备搜索 */
bt_search_device();
/* 查询搜索是否进行中 */
u8 busy = bt_search_status();
/* 用户选择设备后按 MAC 建链 */
emitter_bt_connect(selected_mac);
/* 中途放弃搜索 */
bt_emitter_stop_search_device();
Sources:
示例 3:音频解码器接入(音乐 + 通话)
/* A2DP 音乐解码:打开指定媒体类型解码器 */
extern void *earphone_a2dp_audio_codec_open(int media_type, u8 volume);
/* HFP 通话解码:通话开始/结束 */
extern void *phone_call_begin(void *priv, u8 volume);
extern void phone_call_end(void);
/* eSCO 编解码器占用检测,避免音乐/通话冲突 */
extern u8 get_esco_coder_busy_flag(void);
extern bool get_esco_busy_flag(void);
Sources:
配置选项
经典蓝牙 EDR Demo 的配置分为编译期宏(在 app_config.h 或工程配置中定义)与运行期常量(main.c / bt_emitter.c 内 #define)两类。
编译期宏开关
| 宏 | 作用域 | 说明 |
|---|---|---|
CONFIG_BT_ENABLE | 全局 | 总使能蓝牙功能,main.c 中 #ifdef CONFIG_BT_ENABLE 保护整个 EDR 应用 |
USE_EDR_DEMO | 应用 | 使能 EDR(经典蓝牙)Demo,main.c、bt_decode.c、bt_emitter.c 均以它作为编译门槛 |
TCFG_USER_EMITTER_ENABLE | 应用 | 使能发射器模式,bt_emitter.c 主体逻辑的编译条件 |
BT_MUSIC_DECODE | bt_decode.c | 使能 A2DP 音乐解码路径 |
BT_PHONE_DECODE | bt_decode.c | 使能 HFP 通话解码路径 |
运行期常量
| 常量 | 类型 | 默认值 | 说明 |
|---|---|---|---|
TIMEOUT_CONN_TIME | int | 30 | 连接超时断开后的回连时间(秒) |
POWERON_AUTO_CONN_TIME | int | 18 | 开机自动回连时间(秒),需取 6 的倍数 |
BT_INBAND_RINGTONE | int | 1 | 是否播放手机彩铃(in-band ringtone) |
VOLUME_STEP | int | 5 | 音量调节步进 |
MIN_VOLUME_VALUE | int | 5 | 最小音量值 |
MAX_VOLUME_VALUE | int | 100 | 最大音量值 |
INIT_VOLUME_VALUE | int | 100 | 初始化音量值 |
SBC_FILTER_TIME_MS | int | 2000 | 后台音频过滤时间(毫秒),无有效 SBC 数据时关断解码器 |
SBC_ZERO_TIME_MS | int | 500 | 静音持续多长时间认为已退出播放(毫秒) |
NO_SBC_TIME_MS | int | 100 | 无音频数据判定时间(毫秒) |
SEARCH_LIMITED_MODE | int | SEARCH_BD_NAME_LIMITED (1) | 发射器搜索过滤策略:0=按地址、1=按名字(默认)、2=自定义、3=不过滤 |
inquiry_length | u8 | 20 | 发射器 Inquiry 时长,实际时间 = 值 × 1.28 秒 |
Sources:
API 参考
应用层可见性控制(main.c)
bt_wait_phone_connect_control(u8 enable)
控制本设备可发现(Inquiry Scan)与可连接(Page Scan)状态,根据已连接设备数自动选择档位。
参数:
enable(u8):1 = 使能扫描/连接,0 = 全部关闭
返回: 无
行为:
enable=1且已达最大连接数(is_1t2_connection()为真):SCAN_DISABLE + CONN_DISABLEenable=1且已连 1 台:SCAN_DISABLE + CONN_ENABLEenable=1且未连接:SCAN_ENABLE + CONN_ENABLEenable=0:SCAN_DISABLE + CONN_DISABLE
发射器接口(bt_emitter.c)
void bt_search_device(void)
发起经典蓝牙 Inquiry 设备搜索。搜索前自动关闭本机扫描/连接,搜索持续 inquiry_length × 1.28 秒,通过 bt_search_busy 标志防止重入。
参数: 无 返回: 无
u8 bt_search_status(void)
返回搜索是否正在进行。
返回: bt_search_busy(1 = 搜索中,0 = 空闲)
u8 bt_emitter_role_get(void)
返回当前设备角色。
返回: emitter_or_receiver(BT_EMITTER_EN 表示发射器模式)
void bt_emitter_stop_search_device(void)
通过 USER_CTRL_INQUIRY_CANCEL 中止正在进行的搜索。
参数: 无 返回: 无
void emitter_bt_connect(u8 *mac)
按 6 字节 MAC 地址发起经典蓝牙连接。内部先复位链路(等待建链完成、断开已有连接、取消寻呼),再发送 USER_CTRL_START_CONNEC_VIA_ADDR。
参数:
mac(u8*):目标设备 BD_ADDR(6 字节)
前置条件: 设备必须处于发射器角色(emitter_or_receiver == BT_EMITTER_EN),否则直接返回。
状态查询辅助接口
| 接口 | 说明 |
|---|---|
is_1t2_connection() | 是否已达 1T2 最大连接数 |
get_total_connect_dev() | 当前已连接设备总数 |
hci_standard_connect_check() | 查询 HCI 标准连接状态(0x80 = Profile 建链中,非 0 = 已有连接) |
get_curr_channel_state() | 当前通道状态 |
get_remote_device_info_from_vm() | 从 VM 读取上次配对设备信息 |
get_esco_coder_busy_flag() / get_esco_busy_flag() | eSCO 语音编解码器占用状态 |
Sources:
失败模式、边界情况与并发
连接管理边界
- 1T2 满连接:达到最大连接数后
bt_wait_phone_connect_control()同时关闭 SCAN 与 CONN,此时第三台手机将完全无法发现本设备。若上层仍希望发现,需修改is_1t2_connection()判定或直接调用USER_CTRL_WRITE_SCAN_ENABLE覆盖。 - 发射器角色保护:
emitter_bt_connect()与bt_emitter_start_search_device()入口都检查emitter_or_receiver != BT_EMITTER_EN并直接返回,防止在接收器角色下误发起主动连接。 - 回连失败:开机回连依赖 VM 中保存的
auto_connection_addr[6];若设备已不在范围内,auto_connection_counter计数超限后停止重试,需要用户重新配对(bredr_set_dut_enble()可进入 DUT 测试模式辅助产测)。
音频路径冲突与静音判定
- eSCO 与 A2DP 互斥:通话(eSCO)与音乐(A2DP)共用音频硬件,
get_esco_coder_busy_flag()用于检测编解码器占用。应用通过call_flag记录"因电话切回蓝牙模式",在挂断后依据last_call_type恢复音乐,避免路由错乱。 - 静音退出判定:
SBC_ZERO_TIME_MS = 500表示连续 500ms 静音视为播放退出;SBC_FILTER_TIME_MS = 2000用于过滤后台零星数据。这两个时间窗是功耗与响应速度的折中——太短会频繁开关解码器,太长则退出不及时。 - 彩铃开关:
BT_INBAND_RINGTONE为 0 时播放本地铃声,为 1 时透传手机彩铃;若手机不支持 in-band ringtone,phone_con_sync_ring/phone_con_sync_num_ring标志用于补偿同步逻辑。
并发与竞态防护
- 轮询等待:
emitter_bt_connect()中用while (hci_standard_connect_check() == 0x80) { os_time_dly(10); }忙等 Profile 建链结束,并配合get_curr_channel_state()跳出条件,防止死循环。此类轮询发生在应用任务上下文,os_time_dly(10)主动让出 CPU,不会阻塞系统调度。 - 互斥锁:
bt_emitter.c定义了static OS_MUTEX mutex,保护inquiry_noname_list等搜索结果的链表操作(generic/list.h的list_head),避免搜索回调线程与应用 UI 线程并发遍历。 - 命令串行化:所有协议栈操作都经
user_send_cmd_prepare()进入统一命令队列,由btstack_task串行消费,从根本上规避了多线程直接下发 HCI 命令的竞态。 - 标志位重入保护:
bt_search_busy防止重复搜索;__this->enable保护未使能时的误操作。
性能与运维考虑
- Inquiry 时长与功耗:发射器搜索
20 × 1.28s ≈ 25.6s是功耗敏感操作。产品化时建议结合 UI 提供"搜索中/已停止"提示,并通过bt_emitter_stop_search_device()让用户可随时中止。 - 回连时序设计:
POWERON_AUTO_CONN_TIME = 18秒被设计为 6 的倍数,与协议栈超时粒度对齐,避免回连窗口与内部重传定时器错拍;TIMEOUT_CONN_TIME = 30秒控制断线后的自动重连节奏。 - 解码器开关频率:
NO_SBC_TIME_MS = 100判定无音频后尽快关闭解码器,配合SBC_FILTER_TIME_MS过滤,在"快速响应播放"与"降低静态功耗"之间取得平衡。 - 产测支持:
bredr_set_dut_enble(u8 en, u8 phone)可进入蓝牙 DUT(Device Under Test)模式,配合bredr_close_all_scan()关闭扫描,用于产线射频指标测试。
扩展点
- 新增 Profile:
main.c已集成 A2DP/HFP 的事件回调框架;新增 SPP、PBAP 等 Profile 时,在bt_event分发处扩展分支,并把新状态字段按位域方式追加到struct app_bt_opr。 - 发射器过滤策略:修改
SEARCH_LIMITED_MODE可在"按地址/按名字/自定义/不过滤"间切换;自定义模式(SEARCH_CUSTOM_LIMITED)可在 Inquiry 结果回调中实现白名单/黑名单逻辑。 - 多连接策略定制:
is_1t2_connection()/get_total_connect_dev()的档位判断集中在bt_wait_phone_connect_control(),若产品需要 1T3 或 1T1,只需修改该函数的连接数分支。 - 音量曲线:
VOLUME_STEP/MIN_VOLUME_VALUE/MAX_VOLUME_VALUE/INIT_VOLUME_VALUE集中在main.c顶部,便于按产品调音需求调整。 - 音频服务替换:解码结果通过
server_core.h的音频服务器输出;若使用自定义音频后端,替换earphone_a2dp_audio_codec_open()等外部符号即可,无需改动协议栈。
Related Links
- 示例源码目录:apps/common/example/bluetooth/edr/bt_music/
- 主入口与状态机:main.c
- 音频解码通路:bt_decode.c
- 发射器实现:bt_emitter.c
- 协议栈接口头文件:
btstack/avctp_user.h、btstack/a2dp_media_codec.h、classic/hci_lmp.h、event/bt_event.h - 相关主题:BLE 低功耗蓝牙(见
apps/common/example/bluetooth/ble/下 GATT Client/Server 示例);RCSP 私有协议透传(见apps/common/third_party_profile/jieli/rcsp/bt_trans_data/)